Compare commits
844 commits
trz/merge_
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
5fd91fcd8e | ||
|
cd3ce76115 | ||
|
8a1ebf2bbe | ||
|
0d199c8ceb | ||
|
7651700c2a | ||
|
eea1bc8090 | ||
|
53241f2ef1 | ||
|
e4524e2c7b | ||
|
c9e6d05fa0 | ||
|
be84443921 | ||
|
bbceee7f61 | ||
|
40ee1a0bfc | ||
|
a86b2fefd9 | ||
|
f2d9539d90 | ||
|
66457c9f2e | ||
|
9b6ae6d75f | ||
|
4c6ef3b24e | ||
|
b48bf39e08 | ||
|
7035f38e0b | ||
|
d53c813408 | ||
|
b72d7ec8d0 | ||
|
5dde977233 | ||
|
2f4eee1fa7 | ||
|
96a6460744 | ||
|
780d1daf7e | ||
|
0f870223c4 | ||
|
5faa05ca19 | ||
|
97ba0a0d49 | ||
|
cb9c4d4327 | ||
|
c324f0c8df | ||
|
59f82cbd34 | ||
|
143ad48be1 | ||
|
1dcf804618 | ||
|
ac2eee8e81 | ||
|
764026b87e | ||
|
7219e077f4 | ||
|
bfc2cffc2f | ||
|
f7c5a5c42e | ||
|
9bdd2bf1ae | ||
|
d028f4b398 | ||
|
b085426d22 | ||
|
a71e3d0653 | ||
|
8f39a594ff | ||
|
ebf8ae231a | ||
|
aa7bfe9fe7 | ||
|
b2e9b4aeb1 | ||
|
8e025cbb9e | ||
|
1876b444fa | ||
|
c03e3b5965 | ||
|
fd7216b6a0 | ||
|
585a538340 | ||
|
b050ff2576 | ||
|
bfacc180c5 | ||
|
2c1d3ef968 | ||
|
313baca84e | ||
|
f0c3b31a42 | ||
|
a1cb855739 | ||
|
ef4ed90811 | ||
|
39bb8ad05f | ||
|
b09b8136d2 | ||
|
a994d8f847 | ||
|
b19572ba8c | ||
|
d192c529e0 | ||
|
b116926bb1 | ||
|
39c8867ed7 | ||
|
1269123816 | ||
|
cd772360db | ||
|
4a299920dc | ||
|
e6ba8484fa | ||
|
470d244414 | ||
|
2bb7bc1455 | ||
|
5a670c88b0 | ||
|
fa70bd7536 | ||
|
b8b2051f4c | ||
|
8c34bb3c6f | ||
|
40a9f70478 | ||
|
fcd9ab17fe | ||
|
b8f67bfaa3 | ||
|
82c2e89d21 | ||
|
593dd259a9 | ||
|
c43f224e8b | ||
|
9972f5eabc | ||
|
28c64c2bd1 | ||
|
d03c431137 | ||
|
6c10f8a232 | ||
|
f77afd9596 | ||
|
b011d46ff2 | ||
|
e5fff42b10 | ||
|
fbbf1a37b4 | ||
|
dbda2afd6d | ||
|
87746ca2ba | ||
|
da914ba09c | ||
|
75ee14cfdf | ||
|
55b60f6b0f | ||
|
88321c1e8c | ||
|
8abfbf82fa | ||
|
8d127f70d0 | ||
|
8eb292d16a | ||
|
1739af2a41 | ||
|
b879fb3753 | ||
|
cbc9c1fb20 | ||
|
1e7b4030bb | ||
|
1a89915b31 | ||
|
a5b3c579c4 | ||
|
56991bbaeb | ||
|
6e289b8738 | ||
|
599f7dad2c | ||
|
d4b1119240 | ||
|
6b0242523b | ||
|
5d4aa04e5d | ||
|
58de10bcab | ||
|
e127ba9361 | ||
|
6e95ad4bdf | ||
|
168ad50ddd | ||
|
f0f2aab92d | ||
|
96a992353b | ||
|
c62f3f99be | ||
|
a7ec23ef30 | ||
|
1b9a91eb2f | ||
|
9d744add38 | ||
|
9e7a54849d | ||
|
33e6d8a1ce | ||
|
e5d7357e6e | ||
|
84a2fa0041 | ||
|
bbe01c9a6a | ||
|
fb6f0649c3 | ||
|
fdf19ae287 | ||
|
d983f0bc71 | ||
|
22ca8200fa | ||
|
988cce6320 | ||
|
f4a769080b | ||
|
f36dff485e | ||
|
6320a3ca7c | ||
|
43fd5e5fe6 | ||
|
84a4f4d66d | ||
|
a87f7903c6 | ||
|
1e59a9517a | ||
|
6a5d2e35b5 | ||
|
cbd45d3ee5 | ||
|
2ec7165381 | ||
|
20d3a41b52 | ||
|
839ef8e14b | ||
|
4720ac94d3 | ||
|
07fe434cc7 | ||
|
d2268c6a6f | ||
|
d76b0a3104 | ||
|
1a7e0fd153 | ||
|
6631705aea | ||
|
7b99346a4b | ||
|
1c31b96920 | ||
|
568d6c8392 | ||
|
64e8035f6d | ||
|
b71aa6d3a4 | ||
|
2614706d39 | ||
|
cb639f3fdd | ||
|
6362799d56 | ||
|
40c747660d | ||
|
8132480b82 | ||
|
3bf2876e09 | ||
|
1820b163a1 | ||
|
965f73f95a | ||
|
2b9b3be3f1 | ||
|
a86a36f570 | ||
|
01f92ef4ee | ||
|
d68b7cfcfc | ||
|
fef601b4ae | ||
|
0303c28ad9 | ||
|
1ed2445c1d | ||
|
a7ee8f8a74 | ||
|
9d9a9e63ad | ||
|
99a41265b8 | ||
|
7ec38bd202 | ||
|
211354ee26 | ||
|
7e2e42cb11 | ||
|
3f3b360eee | ||
|
ad9a8c2281 | ||
|
4d965e96ed | ||
|
5007aa1b07 | ||
|
d8bff08f1f | ||
|
ec63900ef3 | ||
|
48afeb571b | ||
|
e84af51272 | ||
|
d61b00604d | ||
|
05fc15be3d | ||
|
6da8b50d95 | ||
|
a753e28ad2 | ||
|
1d3167b520 | ||
|
035d0c7957 | ||
|
bec048407a | ||
|
fe62ef32ae | ||
|
f7c2cd4807 | ||
|
e8cc959a7f | ||
|
bd578c59bf | ||
|
bb4952c89e | ||
|
698ddadbee | ||
|
1ef8d0a746 | ||
|
bca8f11c9c | ||
|
297c0a792f | ||
|
1a57599da2 | ||
|
00b3d5ee35 | ||
|
b3c19f039c | ||
|
d341904c4d | ||
|
7978fd768e | ||
|
b390908610 | ||
|
64ad93dad6 | ||
|
9edbddd7e1 | ||
|
d369ec767f | ||
|
2c004857f6 | ||
|
544c5b4a21 | ||
|
e582b9fc10 | ||
|
e538272417 | ||
|
20ddba2aa9 | ||
|
07a71d312a | ||
|
a5181b22e0 | ||
|
ffebb4677a | ||
|
a44f35ed69 | ||
|
1e4b1a3346 | ||
|
8557120ef8 | ||
|
a4020e85f6 | ||
|
8c1bb058da | ||
|
10398cab51 | ||
|
f2696b66ba | ||
|
52d4be4249 | ||
|
0f62ff6736 | ||
|
44ce5df359 | ||
|
fd4e15ba97 | ||
|
8835f08cf7 | ||
|
c3423d6ffe | ||
|
dce8149aae | ||
|
7226fc0010 | ||
|
50780debf7 | ||
|
f8c21caec9 | ||
|
22d13a3dcd | ||
|
dc02e2b498 | ||
|
6371d2b7a9 | ||
|
2a73b8d76e | ||
|
f6cfa27741 | ||
|
501152bcfd | ||
|
9e54fd5c92 | ||
|
cd1c05a7c3 | ||
|
f7d51b8890 | ||
|
c5bdb04490 | ||
|
74087b873f | ||
|
ad8b9eb054 | ||
|
f3ef8d4978 | ||
|
9efef24a04 | ||
|
5a73a6b139 | ||
|
1f7f82da7b | ||
|
26e33de79a | ||
|
187825d6c6 | ||
|
6d5f23213b | ||
|
0af13fc746 | ||
|
5530b0b0e2 | ||
|
40e5090bdd | ||
|
9f060f477f | ||
|
8d8cb92e43 | ||
|
27af6a4b1e | ||
|
082c06a486 | ||
|
5ac0e9267d | ||
|
cea52b0722 | ||
|
f4a883848c | ||
|
b6e7def9db | ||
|
7c6d1d19d5 | ||
|
dcd6ef8f84 | ||
|
d6c2ff9782 | ||
|
b0fb9fd9ee | ||
|
e275fd8143 | ||
|
43f5dfe174 | ||
|
f0dbcce58f | ||
|
41db773b08 | ||
|
5cd8917122 | ||
|
bb48f67a30 | ||
|
1339b9c464 | ||
|
cee3c98a23 | ||
|
343d895a26 | ||
|
13ed27f91e | ||
|
401759cdc7 | ||
|
61f58b3dbd | ||
|
de7c0c5121 | ||
|
9aaa5b78f4 | ||
|
18ab826413 | ||
|
5790d4c4ab | ||
|
98ab9beec7 | ||
|
7bda624723 | ||
|
7eac903277 | ||
|
badc97e280 | ||
|
7c608c8862 | ||
|
6b904d4de1 | ||
|
858a327299 | ||
|
7bdd4166c0 | ||
|
00c04cd413 | ||
|
3e6747c880 | ||
|
9d0a333372 | ||
|
af55aeca58 | ||
|
521469a57d | ||
|
569b7bf6d0 | ||
|
3cdf5f9afc | ||
|
15c807730e | ||
|
7b445bc4c7 | ||
|
8ca5eb4429 | ||
|
ab63dba8aa | ||
|
4359afacb4 | ||
|
7b52e6984c | ||
|
869ee3d438 | ||
|
d3dfecae8a | ||
|
6cb2b0b5d1 | ||
|
1a0b538166 | ||
|
805717673c | ||
|
e6e46651c9 | ||
|
75fcab3170 | ||
|
59b2e281a3 | ||
|
6fd9888b3b | ||
|
c3b11e515e | ||
|
edf0ae9aa6 | ||
|
00cbf8458a | ||
|
ac9f13a9f2 | ||
|
a54a7dca30 | ||
|
416481bb65 | ||
|
ba6b4763d2 | ||
|
e1d2c32e63 | ||
|
257d1e42d8 | ||
|
7e81149869 | ||
|
1dc55f72e3 | ||
|
d2c475d501 | ||
|
ad09d7dc49 | ||
|
aca7054174 | ||
|
f7d8580969 | ||
|
f14ab4c391 | ||
|
7917c19d18 | ||
|
3685c8cd2a | ||
|
d32cbcc70d | ||
|
af329eff46 | ||
|
b747afb44c | ||
|
2c187d0e7c | ||
|
caafd03130 | ||
|
3d5940cb76 | ||
|
78e962ce67 | ||
|
ea0e6d0619 | ||
|
ad994a2f4c | ||
|
fd54dc5aff | ||
|
76cbb4f727 | ||
|
e33d8451a8 | ||
|
f931c08da7 | ||
|
b52f079292 | ||
|
9e0145a8f6 | ||
|
e98ab37c9d | ||
|
cbda5a5016 | ||
|
910b38ec13 | ||
|
b0cdc2745c | ||
|
2e4713897d | ||
|
542626758d | ||
|
a4d342683e | ||
|
0b9d38cf32 | ||
|
f1ecbf2ff8 | ||
|
6f72128c45 | ||
|
8927ba8065 | ||
|
a0038565c5 | ||
|
285d86b375 | ||
|
cf909afc60 | ||
|
2a139a4b47 | ||
|
0528a47b8a | ||
|
b5d3859b22 | ||
|
343bb7ff28 | ||
|
94aee445e7 | ||
|
4736d12e99 | ||
|
eb8b6165d7 | ||
|
81b0f60860 | ||
|
8b6f06f0f9 | ||
|
08725ba2bb | ||
|
9bfdbc708e | ||
|
856029a611 | ||
|
a51de9fcd9 | ||
|
121312d103 | ||
|
d02e24248f | ||
|
8b331895d1 | ||
|
ed2fa20414 | ||
|
9dc8e3db9d | ||
|
1b114beb0b | ||
|
3c48b14448 | ||
|
0e96e0a796 | ||
|
c06aceaae9 | ||
|
04976fe333 | ||
|
178229ac60 | ||
|
dbab43e423 | ||
|
cf7df84cab | ||
|
701140fe92 | ||
|
58a3ef46ce | ||
|
82908fb54b | ||
|
3409399ef1 | ||
|
198a9f2226 | ||
|
89a05265ea | ||
|
3d372cb339 | ||
|
6dcce76568 | ||
|
3a5735e717 | ||
|
e9c00c0427 | ||
|
c8188ee52c | ||
|
2843a0af26 | ||
|
e90e333f29 | ||
|
eb3ac1c326 | ||
|
3e50d4831f | ||
|
0bc5dbdf94 | ||
|
baa149924a | ||
|
1db85e582e | ||
|
2803d342e1 | ||
|
223d50c1a0 | ||
|
27690865a6 | ||
|
58d5d2a1be | ||
|
ff1b23b4d9 | ||
|
be4aa2afc9 | ||
|
01a4d2ea25 | ||
|
f9aca85edf | ||
|
57e51bc735 | ||
|
cdee91363c | ||
|
ac8aa63916 | ||
|
369e7172d6 | ||
|
09aba0a062 | ||
|
9efa242d96 | ||
|
30110431ba | ||
|
91c3732c63 | ||
|
f7933c26d7 | ||
|
1d79a677c8 | ||
|
b5caa8fa35 | ||
|
8882c6b6fd | ||
|
e63d6b4bf2 | ||
|
9a7f51520e | ||
|
4e6d16c49b | ||
|
e52f662569 | ||
|
72a2622c84 | ||
|
97fe14c4be | ||
|
78e3afc1af | ||
|
d2ca0c7fe8 | ||
|
4d5e0c291e | ||
|
982a20fef5 | ||
|
4ba5472d0c | ||
|
d28d968985 | ||
|
34454ef2ec | ||
|
4d1640d6ff | ||
|
e88f01923f | ||
|
1166619539 | ||
|
28dc888159 | ||
|
ea1e4c773d | ||
|
37e7175a86 | ||
|
85c82d9b3b | ||
|
829720409d | ||
|
f91d16cbe7 | ||
|
b92b3863b9 | ||
|
fc3aefd56e | ||
|
dcc13d7a3d | ||
|
48a7818e88 | ||
|
1eb776f39c | ||
|
f8b1e8098c | ||
|
60588af825 | ||
|
f99f21ab9b | ||
|
5f4471a45e | ||
|
cb5393c32f | ||
|
5f40a7042d | ||
|
e0575642b5 | ||
|
73679b97f1 | ||
|
49de43b364 | ||
|
f9600b950f | ||
|
95a51ea2e0 | ||
|
39ad426ca9 | ||
|
40f81f19df | ||
|
587fb3cca3 | ||
|
490a1ca3cf | ||
|
ea667a1a73 | ||
|
f4e3cd5098 | ||
|
c4680e3198 | ||
|
31dd7b5a21 | ||
|
74d376be68 | ||
|
5017e8564c | ||
|
a70f57358e | ||
|
4bf9a1e809 | ||
|
e2a803ee04 | ||
|
4b9b7257a9 | ||
|
cb7c47bc62 | ||
|
33a02faad9 | ||
|
a018935b23 | ||
|
112a4d389e | ||
|
7932244c51 | ||
|
b88128241e | ||
|
9f42ead747 | ||
|
92bad0fa1e | ||
|
d089ceac13 | ||
|
8e6f054e52 | ||
|
36ae840d76 | ||
|
7a97da6d21 | ||
|
794353ad0c | ||
|
71e9117176 | ||
|
6639d0f23b | ||
|
becc3eb867 | ||
|
7398424f3b | ||
|
e26d842549 | ||
|
17c62b5991 | ||
|
161fdf7340 | ||
|
e402348f9b | ||
|
583aba1b44 | ||
|
594aab56db | ||
|
25211f13b3 | ||
|
e43a01159c | ||
|
45cc33ca36 | ||
|
20ba1add1e | ||
|
91732b89ea | ||
|
add8e2cb74 | ||
|
15316e6a7f | ||
|
5c5d5cc4e3 | ||
|
24ea66c9fc | ||
|
ffba53777c | ||
|
ea6a008b39 | ||
|
1838023c88 | ||
|
b3337c4ad7 | ||
|
b7c8ce1511 | ||
|
6d0e5f4354 | ||
|
5b9ba79495 | ||
|
9321ccc775 | ||
|
8eb1640a26 | ||
|
be0fc59314 | ||
|
272cffe797 | ||
|
762820072a | ||
|
ea18ceae4a | ||
|
71787bd2e1 | ||
|
49cefd1c0c | ||
|
9afafe387a | ||
|
107ab85a22 | ||
|
d89d7ade84 | ||
|
c3ec3ea70a | ||
|
2c55954ddd | ||
|
aaf5233efe | ||
|
422fd1847f | ||
|
ce0888b077 | ||
|
fde27f447f | ||
|
b3f50d1ad0 | ||
|
bc326efd2c | ||
|
bc36f1950f | ||
|
ae7543bbfc | ||
|
06bef5de8d | ||
|
25f6651848 | ||
|
29bd1103c0 | ||
|
a241ab66de | ||
|
f70fcc7bb8 | ||
|
44833c1499 | ||
|
82c3cbaf2a | ||
|
21ebb35e44 | ||
|
d9ff61ea2e | ||
|
841e718d6a | ||
|
c4e82eb3f8 | ||
|
c06e2787c7 | ||
|
83adbb6052 | ||
|
5137837f6d | ||
|
c65c314801 | ||
|
79796b0079 | ||
|
b69ab65b12 | ||
|
abbdf232c6 | ||
|
d84cf4e6d1 | ||
|
e5b8302fd9 | ||
|
33218ec32a | ||
|
a8420c9ad0 | ||
|
277e3d59c8 | ||
|
e1cf7b8cb6 | ||
|
9ce2cfa3d2 | ||
|
8d595c1fc2 | ||
|
ef27055434 | ||
|
3f65b0e985 | ||
|
70497318dd | ||
|
0eb8d4226e | ||
|
627bf18f8c | ||
|
afa3883089 | ||
|
b478eca315 | ||
|
61726f4994 | ||
|
14952ba5e5 | ||
|
fc5304c6fe | ||
|
8d0693ed6a | ||
|
d7c5264ad0 | ||
|
331cbf3696 | ||
|
6f1a4494eb | ||
|
cf5ca27a06 | ||
|
c9e9dc2ef2 | ||
|
a25912c32c | ||
|
540f6f3d7a | ||
|
018f978a22 | ||
|
6a28b5a9fa | ||
|
e41a9483bd | ||
|
aced9d2697 | ||
|
b756d61c45 | ||
|
e6ff1539b4 | ||
|
72f541140f | ||
|
acad161344 | ||
|
b8c1bd2cba | ||
|
2014f388b1 | ||
|
cbdb413613 | ||
|
f4369b29ae | ||
|
7113e21a43 | ||
|
908aa19a36 | ||
|
09e20f6e01 | ||
|
1bc92482e9 | ||
|
cc209afc51 | ||
|
8e3948e495 | ||
|
c37b5af2ca | ||
|
e542dd3923 | ||
|
549be9bb3d | ||
|
27b245ac35 | ||
|
488780d2ce | ||
|
6f3b8f64d1 | ||
|
784df0c218 | ||
|
fb7525e0b9 | ||
|
76889b9c58 | ||
|
e2d3bef739 | ||
|
a7cd05bd4e | ||
|
0157039e87 | ||
|
544e1dee65 | ||
|
6e0ec9b924 | ||
|
12704fa640 | ||
|
8a81f85734 | ||
|
c27663c456 | ||
|
fb41a4ffaa | ||
|
16eb1bfbd0 | ||
|
b334582eff | ||
|
7047d68165 | ||
|
dee7fd3eab | ||
|
cf374ec4ef | ||
|
59f02f7766 | ||
|
cef2eb58a7 | ||
|
fad8b702aa | ||
|
cfa31beaf7 | ||
|
f444390617 | ||
|
06a561743a | ||
|
7674e01585 | ||
|
bf92ef6cd3 | ||
|
d23178acb9 | ||
|
98ecac0ffa | ||
|
936006173c | ||
|
d5608cb4f3 | ||
|
c7882b7225 | ||
|
6d9ca25915 | ||
|
252d015b71 | ||
|
1d2e2f71c2 | ||
|
51753a1d39 | ||
|
5021b9a5dd | ||
|
29616d02a8 | ||
|
ebcb13c8eb | ||
|
e6b526230a | ||
|
9c3e910dc4 | ||
|
59652ecaf2 | ||
|
6a677a172b | ||
|
94983ca3ed | ||
|
a363e0a5d8 | ||
|
cd1fbf60ec | ||
|
a9c1768107 | ||
|
1901abd05f | ||
|
195b745efc | ||
|
1a073ca454 | ||
|
bfe01c4322 | ||
|
e9494af098 | ||
|
eb63cdb9ad | ||
|
72aa10b536 | ||
|
39e717ed94 | ||
|
c53c6cb6b6 | ||
|
594e65bb2b | ||
|
4332b0df44 | ||
|
3e654bea0e | ||
|
2a4db01709 | ||
|
7223b5b274 | ||
|
7ff890e513 | ||
|
23a0beab43 | ||
|
77f4513862 | ||
|
677269606c | ||
|
5786e75374 | ||
|
91b17c6925 | ||
|
607b7d1593 | ||
|
83fab06508 | ||
|
4652541b61 | ||
|
65548ddccb | ||
|
b99d70bfe7 | ||
|
2713fd50c8 | ||
|
14b46c3ee7 | ||
|
a8ebc5fafc | ||
|
c22b384680 | ||
|
db0301310b | ||
|
7a84cfdfa2 | ||
|
c55f7645a4 | ||
|
0460702710 | ||
|
290f0a123e | ||
|
275d6a858c | ||
|
b4ad2de2e5 | ||
|
ecaf75e5ec | ||
|
a968260b18 | ||
|
0385e3a8d6 | ||
|
e94e06246b | ||
|
5787687997 | ||
|
61997912fd | ||
|
5eedce91f9 | ||
|
701742f550 | ||
|
2549ce89b0 | ||
|
74c496fe3e | ||
|
e074104004 | ||
|
867d0ef191 | ||
|
8d98c52803 | ||
|
343a6b4e6b | ||
|
d115f38361 | ||
|
1d458e8ab3 | ||
|
46be514b4d | ||
|
a9b66e3ea5 | ||
|
281cb65046 | ||
|
564113669e | ||
|
0baa2dd03e | ||
|
6ba90ec43c | ||
|
135c8567a5 | ||
|
ac09011690 | ||
|
7df24407dc | ||
|
b51ce43d36 | ||
|
c4b1f6171d | ||
|
b17ca3543f | ||
|
48be5af55f | ||
|
323d31ba05 | ||
|
eaddfa7fd1 | ||
|
678bc7b4d4 | ||
|
815c534da8 | ||
|
0af8ee341c | ||
|
1153e6120d | ||
|
290f53f4a6 | ||
|
817d344521 | ||
|
9548f43998 | ||
|
7eb736227e | ||
|
1e75283250 | ||
|
24aefa109c | ||
|
e6a9829dd2 | ||
|
86fff5839a | ||
|
8339ebf3dc | ||
|
d3542202b5 | ||
|
e9b4a2a021 | ||
|
09d87965fb | ||
|
aa24a0f779 | ||
|
89eea3636f | ||
|
07263370d9 | ||
|
cc67bfd8db | ||
|
bc5f64bffe | ||
|
4cb2d0ca93 | ||
|
c9e4b332bf | ||
|
aaf64732b0 | ||
|
15a1873d97 | ||
|
fd246f7e5a | ||
|
ab4d86dde7 | ||
|
198dc2c6b4 | ||
|
df7b399e04 | ||
|
134c75ae01 | ||
|
9e0466d1e6 | ||
|
199ae3a4d8 | ||
|
4ba41540fd | ||
|
5cdfd0ec50 | ||
|
24a9ac2908 | ||
|
2c224d0f18 | ||
|
3cf21e2d37 | ||
|
60ab03afb1 | ||
|
c393e60891 | ||
|
7fd6a37e67 | ||
|
dc00a92499 | ||
|
5d3ee60ca4 | ||
|
bbede8bbeb | ||
|
e1a2f248af | ||
|
a88c2d48c0 | ||
|
d1a456f3e3 | ||
|
ddafa65849 | ||
|
17b1fcc3ea | ||
|
34f2a63190 | ||
|
0298f0181e | ||
|
894b5892a9 | ||
|
20eebe638b | ||
|
ad063d00cc | ||
|
beb216c300 | ||
|
7f45e210af | ||
|
f1c947f0d6 | ||
|
9ae997cab8 | ||
|
4a9753bebc | ||
|
5eb2d9af83 | ||
|
689ded1607 | ||
|
a0d0ed34ae | ||
|
c20d8ac69e | ||
|
d2cfac222e | ||
|
b00c561f81 | ||
|
ed740b4868 | ||
|
43b466704a | ||
|
3bde4dbedb | ||
|
e6f8b7d9fa | ||
|
a2cb009f4c | ||
|
df992d2566 | ||
|
ad60bc002c | ||
|
49a3f6f281 | ||
|
ac687d6bbd | ||
|
59978e157c | ||
|
3626e4b3a0 | ||
|
c2fbdbde83 | ||
|
86b1865eec | ||
|
726393f8da | ||
|
349dd8291d | ||
|
c39008983e | ||
|
a27cbfbf56 | ||
|
7d63b06d84 | ||
|
d06013fbaf | ||
|
a5e40672c1 | ||
|
a9b957e8a2 | ||
|
0ca4a33bfb | ||
|
c0b3a3ff0c | ||
|
335058b78b | ||
|
c4b1df1bf3 | ||
|
c3f0503a91 | ||
|
8ccb2005b3 | ||
|
356199978e | ||
|
92a6e956fd | ||
|
300326fba3 | ||
|
251f2479c2 | ||
|
6f9f871928 | ||
|
c7a14092a8 | ||
|
6217e33a87 | ||
|
c430848ade | ||
|
bac249c8dd | ||
|
32da65f910 | ||
|
93dad9b737 | ||
|
d58d822215 | ||
|
f37098a54f | ||
|
1bb38e25f2 | ||
|
f16690ae1f | ||
|
91ec4839ac | ||
|
28733e052f | ||
|
4fdb0d92fe | ||
|
f88b8c703e | ||
|
17791a703e | ||
|
7dd9545ea3 | ||
|
1d572c61d0 | ||
|
0911669b07 | ||
|
1274b0ef39 | ||
|
f0798216d5 | ||
|
4a1a59f0c8 | ||
|
01bad12708 | ||
|
58c6f9bfb2 | ||
|
fab0a45955 | ||
|
ba9ba8ffe2 | ||
|
f30df7a535 | ||
|
ae84ff2f0c | ||
|
000f59d614 | ||
|
bf5b2f73f5 | ||
|
ad36a4ba89 | ||
|
56f8fff935 | ||
|
1e335d527b | ||
|
fccce229c6 |
11
.github/CONTRIBUTING.md
vendored
Normal file
|
@ -0,0 +1,11 @@
|
|||
### Reporting Bugs and getting help
|
||||
|
||||
The issue tracker on Github is for bug reports only. It is not a general support forum.
|
||||
|
||||
**A bug is everything you can reproduce. ie *if I do this that happens but something else should happen instead*.**
|
||||
|
||||
Please search the issue tracker (including closed issues). Your bug might be a server bug that has already been addressed.
|
||||
|
||||
Please fill in the template when creating new issues and provide information about your device and your server. The [adb logcat](https://wiki.cyanogenmod.org/w/Doc:_debugging_with_logcat) can be omitted if you can reproduce the bug under every condition. (ie *click button X and the application crashes*) But the adb logcat is required if the bug happens only under certain conditions or involves network problem (Server not found, messages not arriving)
|
||||
|
||||
**If you are seeking help or are unable to reproduce the exact problem please join our MUC at conversations@conference.siacs.eu**
|
30
.github/ISSUE_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
#### General information
|
||||
|
||||
* **Version:** eg 1.12.2
|
||||
* **Device:** eg Google Nexus 5
|
||||
* **Android Version:** eg Android 6.0 Stock or Android 5.1 Cyanogenmod
|
||||
* **Server name:** eg conversations.im, jabber.at or self hosted
|
||||
* **Server software:** ejabberd 16.04 or prosody 0.10 (if known)
|
||||
* **Installed server modules:** eg Stream Managment, CSI, MAM
|
||||
* **Conversations source:** eg PlayStore, PlayStore Beta Channel, F-Droid, self build (latest HEAD)
|
||||
|
||||
|
||||
#### Steps to reproduce
|
||||
|
||||
1. …
|
||||
2. …
|
||||
|
||||
|
||||
#### Expected result
|
||||
|
||||
What is the expected output? What do you see instead?
|
||||
|
||||
|
||||
#### Debug output
|
||||
|
||||
Please post the output of adb logcat. The log should begin with the start of Conversations and include all the
|
||||
steps it takes to reproduce the problem.
|
||||
|
||||
````
|
||||
adb logcat -s conversations
|
||||
````
|
4
.gitignore
vendored
|
@ -2,9 +2,13 @@
|
|||
*.swp
|
||||
.settings
|
||||
|
||||
src/playstore/res/values/gcm.xml
|
||||
|
||||
# https://github.com/github/gitignore/blob/master/Gradle.gitignore
|
||||
.gradle/
|
||||
build/
|
||||
captures/
|
||||
gradle.properties
|
||||
# Ignore Gradle GUI config
|
||||
gradle-app.setting
|
||||
|
||||
|
|
17
.travis.yml
|
@ -1,13 +1,16 @@
|
|||
language: android
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
android:
|
||||
components:
|
||||
- platform-tools
|
||||
- tools
|
||||
- build-tools-23.0.2
|
||||
- build-tools-23.0.1
|
||||
- build-tools-23.0.0
|
||||
- build-tools-22.0.1
|
||||
- build-tools-21.1.2
|
||||
- build-tools-19.1.0
|
||||
- android-23
|
||||
- build-tools-25.0.2
|
||||
- android-25
|
||||
- extra-android-m2repository
|
||||
- extra-google-m2repository
|
||||
- extra-google-google_play_services
|
||||
licenses:
|
||||
- '.+'
|
||||
script:
|
||||
- ./gradlew assembleFreeRelease
|
||||
|
|
196
CHANGELOG.md
|
@ -1,5 +1,195 @@
|
|||
###Changelog
|
||||
|
||||
####Version 1.15.5
|
||||
* show nick as bold text when mentioned in conference
|
||||
* bug fixes
|
||||
|
||||
####Version 1.15.4
|
||||
* bug fixes
|
||||
|
||||
####Version 1.15.3
|
||||
* show offline contacts in MUC as grayed-out
|
||||
* don't transcode gifs. add overlay indication to gifs
|
||||
* bug fixes
|
||||
|
||||
####Version 1.15.2
|
||||
* bug fixes
|
||||
|
||||
####Version 1.15.1
|
||||
* support for POSH (RFC7711)
|
||||
* support for quoting messages (via select text)
|
||||
* verified messages show shield icon. unverified messages show lock
|
||||
|
||||
####Version 1.15.0
|
||||
* New [Blind Trust Before Verification](https://gultsch.de/trust.html) mode
|
||||
* Easily share Barcode and XMPP uri from Account details
|
||||
* Automatically deactivate own devices after 7 day of inactivity
|
||||
* Improvements fo doze/push mode
|
||||
* bug fixes
|
||||
|
||||
####Version 1.14.9
|
||||
* warn in account details when data saver is enabled
|
||||
* automatically enable foreground service after detecting frequent restarts
|
||||
* bug fixes
|
||||
|
||||
####Version 1.14.8
|
||||
* bug fixes
|
||||
|
||||
####Version 1.14.7
|
||||
* error message accessible via context menu for failed messages
|
||||
* don't include pgp signature in anonymous mucs
|
||||
* bug fixes
|
||||
|
||||
####Version 1.14.6
|
||||
* make error notification dismissable
|
||||
* bug fixes
|
||||
|
||||
|
||||
####Version 1.14.5
|
||||
* expert setting to delete OMEMO identities
|
||||
* bug fixes
|
||||
|
||||
####Version 1.14.4
|
||||
* bug fixes
|
||||
|
||||
####Version 1.14.3
|
||||
* XEP-0377: Spam Reporting
|
||||
* fix rare start up crashes
|
||||
|
||||
####Version 1.14.2
|
||||
* support ANONYMOUS SASL
|
||||
* bug fixes
|
||||
|
||||
####Version 1.14.1
|
||||
* Press lock icon to see why OMEMO is deactivated
|
||||
* bug fixes
|
||||
|
||||
####Version 1.14.0
|
||||
* Improvments for N
|
||||
* Quick Reply to Notifications on N
|
||||
* Don't download avatars and files when data saver is on
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.9
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.8
|
||||
* show identities instead of resources in selection dialog
|
||||
* allow TLS direct connect when port is set to 5223
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.7
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.6
|
||||
* thumbnails for videos
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.5
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.4
|
||||
* support jingle ft:4
|
||||
* show contact as DND if one resource is
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.3
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.2
|
||||
* new PGP decryption logic
|
||||
* bug fixes
|
||||
|
||||
####Version 1.13.1
|
||||
* changed some colors in dark theme
|
||||
* fixed fall-back message for OMEMO
|
||||
|
||||
####Version 1.13.0
|
||||
* configurable dark theme
|
||||
* opt-in to share Last User Interaction
|
||||
|
||||
####Version 1.12.9
|
||||
* make grace period configurable
|
||||
|
||||
####Version 1.12.8
|
||||
* more bug fixes :-(
|
||||
|
||||
####Version 1.12.7
|
||||
* bug fixes
|
||||
|
||||
####Version 1.12.6
|
||||
* bug fixes
|
||||
|
||||
####Version 1.12.5
|
||||
* new create conference dialog
|
||||
* show first unread message on top
|
||||
* show geo uri as links
|
||||
* circumvent long message DOS
|
||||
|
||||
####Version 1.12.4
|
||||
* show offline members in conference (needs server support)
|
||||
* various bug fixes
|
||||
|
||||
####Version 1.12.3
|
||||
* make omemo default when all resources support it
|
||||
* show presence of other resources as template
|
||||
* start typing in StartConversationsActivity to search
|
||||
* various bug fixes and improvements
|
||||
|
||||
####Version 1.12.2
|
||||
* fixed pgp presence signing
|
||||
|
||||
####Version 1.12.1
|
||||
* small bug fixes
|
||||
|
||||
####Version 1.12.0
|
||||
* new welcome screen that makes it easier to register account
|
||||
* expert setting to modify presence
|
||||
|
||||
####Version 1.11.7
|
||||
* Share xmpp uri from conference details
|
||||
* add setting to allow quick sharing
|
||||
* various bug fixes
|
||||
|
||||
####Version 1.11.6
|
||||
* added preference to disable notification light
|
||||
* various bug fixes
|
||||
|
||||
####Version 1.11.5
|
||||
* check file ownership to not accidentally share private files
|
||||
|
||||
####Version 1.11.4
|
||||
* fixed a bug where contacts are shown as offline
|
||||
* improved broken PEP detection
|
||||
|
||||
####Version 1.11.3
|
||||
* check maximum file size when using HTTP Upload
|
||||
* properly calculate caps hash
|
||||
|
||||
####Version 1.11.2
|
||||
* only add image files to media scanner
|
||||
* allow to delete files
|
||||
* various bug fixes
|
||||
|
||||
####Version 1.11.1
|
||||
* fixed some bugs when sharing files with Conversations
|
||||
|
||||
####Version 1.11.0
|
||||
* OMEMO encrypted conferences
|
||||
|
||||
####Version 1.10.1
|
||||
* made message correction opt-in
|
||||
* various bug fixes
|
||||
|
||||
####Version 1.10.0
|
||||
* Support for XEP-0357: Push Notifications
|
||||
* Support for XEP-0308: Last Message Correction
|
||||
* introduced build flavors to make dependence on play-services optional
|
||||
|
||||
####Version 1.9.4
|
||||
* prevent cleared Conversations from reloading history with MAM
|
||||
* various MAM fixes
|
||||
|
||||
####Version 1.9.3
|
||||
* expert setting that enables host and port configuration
|
||||
* expert setting opt-out of bookmark autojoin handling
|
||||
|
@ -123,7 +313,7 @@
|
|||
|
||||
####Version 1.4.0
|
||||
* send button turns into quick action button to offer faster access to take photo, send location or record audio
|
||||
* visually seperate merged messages
|
||||
* visually separate merged messages
|
||||
* faster reconnects of failed accounts after network switches
|
||||
* r/o vcard avatars for contacts
|
||||
* various bug fixes
|
||||
|
@ -216,7 +406,7 @@
|
|||
* Download HTTP images
|
||||
* Show avatars in MUC tiles
|
||||
* Disabled SSLv3
|
||||
* Performance improvments
|
||||
* Performance improvements
|
||||
* bug fixes
|
||||
|
||||
####Version 0.7.3
|
||||
|
@ -265,7 +455,7 @@
|
|||
####Version 0.4
|
||||
* OTR file encryption
|
||||
* keep OTR messages and files on device until both parties or online at the same time
|
||||
* XEP-0333. Mark wether the other party has read your messages
|
||||
* XEP-0333. Mark whether the other party has read your messages
|
||||
* Delayed messages are now tagged properly
|
||||
* Share images from the Gallery
|
||||
* Infinit history scrolling
|
||||
|
|
128
README.md
|
@ -16,7 +16,7 @@ Conversations: the very last word in instant messaging
|
|||
|
||||
## Features
|
||||
|
||||
* End-to-end encryption with [OMEMO](http://conversations.im/omemo/), [OTR](https://otr.cypherpunks.ca/), or [OpenPGP](http://www.openpgp.org/about_openpgp/)
|
||||
* End-to-end encryption with [OMEMO](http://conversations.im/omemo/), [OTR](https://otr.cypherpunks.ca/), or [OpenPGP](http://openpgp.org/about/)
|
||||
* Send and receive images as well as other kind of files
|
||||
* Share your location via an external [plug-in](https://play.google.com/store/apps/details?id=eu.siacs.conversations.sharelocation&referrer=utm_source%3Dgithub)
|
||||
* Indication when your contact has read your message
|
||||
|
@ -56,9 +56,8 @@ run your own XMPP server for you and your friends. These XEP's are:
|
|||
* [XEP-0352: Client State Indication](http://xmpp.org/extensions/xep-0352.html) lets the server know whether or not
|
||||
Conversations is in the background. Allows the server to save bandwidth by
|
||||
withholding unimportant packages.
|
||||
* [XEP-0363: HTTP File Upload](http://xmpp.org/extensions/xep-0363.html) allows you to share files in conferences and with offline
|
||||
contacts. Requires an [additional component](https://github.com/siacs/HttpUploadComponent)
|
||||
on your server. Alternatively, an [Ejabberd contrib-module](https://github.com/processone/ejabberd-contrib/tree/master/mod_http_upload) and a [Prosody module](http://modules.prosody.im/mod_http_upload.html) are available.
|
||||
* [XEP-0363: HTTP File Upload](http://xmpp.org/extensions/xep-0363.html) allows you to share files in conferences
|
||||
and with offline contacts.
|
||||
|
||||
## Team
|
||||
|
||||
|
@ -119,20 +118,27 @@ My Bitcoin Address is: `1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu`
|
|||
[![Flattr this!](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=inputmice&url=http%3A%2F%2Fconversations.siacs.eu&title=Conversations&tags=github&category=software)
|
||||
|
||||
#### How do I create an account?
|
||||
XMPP, like email, is a federated protocol, which means that there is not one company you can create an *official XMPP account* with. Instead there are hundreds, or even thousands, of providers out there. One of those providers is our very own [conversations.im](https://account.conversations.im). If you don’t like to use *conversations.im* use a web search engine of your choice to find another provider. Or maybe your university has one. Or you can run your own. Or ask a friend to run one. Once you've found one, you can use Conversations to create an account. Just select *register new account* on server within the create account dialog.
|
||||
|
||||
XMPP, like email, is a federated protocol which means that there is not one
|
||||
company you can create an 'official XMPP account' with. Instead there are
|
||||
hundreds, or even thousands, of provider out there. To find one use a web search
|
||||
engine of your choice. Or maybe your university has one. Or you can run your
|
||||
own. Or ask a friend to run one. Once you've found one, you can use
|
||||
Conversations to create an account. Just select 'register new account on server'
|
||||
within the create account dialog.
|
||||
##### Domain hosting
|
||||
Using your own domain not only gives you a more recognizable Jabber ID, it also gives you the flexibility to migrate your account between different XMPP providers. This is a good compromise between the responsibilities of having to operate your own server and the downsides of being dependent on a single provider.
|
||||
|
||||
Learn more about [conversations.im Jabber/XMPP domain hosting](https://account.conversations.im/domain/).
|
||||
|
||||
##### Running your own
|
||||
If you already have a server somewhere and are willing and able to put the necessary work in, one alternative-in the spirit of federation-is to run your own. We recommend either [Prosody](https://prosody.im/) or [ejabberd](https://www.ejabberd.im/). Both of which have their own strengths. Ejabberd is slightly more mature nowadays but Prosody is arguably easier to set up.
|
||||
|
||||
For Prosody you need a couple of so called [community modules](https://modules.prosody.im/) most of which are maintained by the same people that develop Prosody.
|
||||
|
||||
If you pick ejabberd make sure you use the latest version. Linux Distributions might bundle some very old versions of it.
|
||||
|
||||
#### Where can I set up a custom hostname / port
|
||||
Conversations will automatically look up the SRV records for your domain name
|
||||
which can point to any hostname port combination. If your server doesn’t provide
|
||||
those please contact your admin and have them read
|
||||
[this](http://prosody.im/doc/dns#srv_records)
|
||||
[this](http://prosody.im/doc/dns#srv_records). If your server operator is unwilling
|
||||
to fix this you can enable advanced server settings in the expert settings of
|
||||
Conversations.
|
||||
|
||||
#### I get 'Incompatible Server'
|
||||
|
||||
|
@ -146,6 +152,15 @@ On rare occasions this error message might also be caused by a server not provid
|
|||
a login (SASL) mechanism that Conversations is able to handle. Conversations supports
|
||||
SCRAM-SHA1, PLAIN, EXTERNAL (client certs) and DIGEST-MD5.
|
||||
|
||||
#### How do XEP-0357: Push Notifications work?
|
||||
You need to be running the Play Store version of Conversations and your server needs to support push notifications.¹ Because *Google Cloud Notifications (GCM)* are tied with an API key to a specific app your server can not initiate the push message directly. Instead your server will send the push notification to the Conversations App server (operated by us) which then acts as a proxy and initiates the push message for you. The push message sent from our App server through GCM doesn’t contain any personal information. It is just an empty message which will wake up your device and tell Conversations to reconnect to your server. The information send from your server to our App server depends on the configuration of your server but can be limited to your account name. (In any case the Conversations App server won't redirect any information through GCM even if your server sends this information.)
|
||||
|
||||
In summary Google will never get hold of any personal information besides that *something* happened. (Which doesn’t even have to be a message but can be some automated event as well.) We - as the operator of the App server - will just get hold of your account name (without being able to tie this to your specific device).
|
||||
|
||||
If you don’t want this simply pick a server which does not offer Push Notifications or build Conversations yourself without support for push notifications. (This is available via a gradle build flavor.) Non-play store source of Conversations like the Amazon App store will also offer a version without push notifications. Conversations will just work as before and maintain its own TCP connection in the background.
|
||||
|
||||
¹ Your server only needs to support the server side of [XEP-0357: Push Notifications](http://xmpp.org/extensions/xep-0357.html). If you use the Play Store version you do **not** need to run your own app server. The server modules are called *mod_cloud_notify* on Prosody and *mod_push* on ejabberd.
|
||||
|
||||
#### Conversations doesn't work for me. Where can I get help?
|
||||
|
||||
You can join our conference room on `conversations@conference.siacs.eu`.
|
||||
|
@ -186,6 +201,12 @@ connection again. When the client fails to do so because the network
|
|||
connectivity is out for longer than that all messages sent to that client will
|
||||
be returned to the sender resulting in a delivery failed.
|
||||
|
||||
Instead of returning a message to the sender both ejabberd and prosody have the
|
||||
ability to store messages in offline storage when the disconnecting client is
|
||||
the only client. In prosody this is available via an extra module called
|
||||
```mod_smacks_offline```. In ejabberd this is available via some configuration
|
||||
settings.
|
||||
|
||||
Other less common reasons are that the message you sent didn't meet some
|
||||
criteria enforced by the server (too large, too many). Another reason could be
|
||||
that the recipient is offline and the server doesn't provide offline storage.
|
||||
|
@ -226,6 +247,11 @@ Making these status and priority optional isn't a solution either because
|
|||
Conversations is trying to get rid of old behaviours and set an example for
|
||||
other clients.
|
||||
|
||||
#### How do I backup / move Conversations to a new device?
|
||||
On the one hand Conversations supports Message Archive Management to keep a server side history of your messages so when migrating to a new device that device can display your entire history. However that does not work if you enable OMEMO due to its forward secrecy. (Read [The State of Mobile XMPP in 2016](https://gultsch.de/xmpp_2016.html) especially the section on encryption.)
|
||||
|
||||
If you migrate to a new device and would still like to keep your history please use a third party backup tool like [oandbackup](https://github.com/jensstein/oandbackup) or ```adb backup``` from your computer. It is important that your deactivate your account before backup and activate it only after a successful restore. Otherwise OMEMO might not work afterwards.
|
||||
|
||||
#### Conversations is missing a certain feature
|
||||
|
||||
I'm open for new feature suggestions. You can use the [issue tracker][issues] on
|
||||
|
@ -250,7 +276,9 @@ I am available for hire. Contact me via XMPP: `inputmice@siacs.eu`
|
|||
|
||||
#### Why are there three end-to-end encryption methods and which one should I choose?
|
||||
|
||||
In most cases OTR should be the encryption method of choice. It works out of the box with most contacts as long as they are online. However, openPGP can, in some cases, (message carbons to multiple clients) be more flexible. Unlike OTR, OMEMO works even when a contact is offline, and works with multiple devices. It also allows asynchronous file-transfer when the server has [HTTP File Upload](http://xmpp.org/extensions/xep-0363.html). However, OMEMO is not as widely supported as OTR and is currently implemented only by Conversations. OMEMO should be preffered over OTR for contacts who use Conversations.
|
||||
* OTR is a legacy encryption method. It works out of the box with most contacts as long as they are online.
|
||||
* OMEMO works even when a contact is offline, and works with multiple devices. It also allows asynchronous file-transfer when the server has [HTTP File Upload](http://xmpp.org/extensions/xep-0363.html). However, OMEMO is not as widely supported as OTR and is currently implemented only by Conversations and Gajim. OMEMO should be preferred over OTR for contacts who use Conversations.
|
||||
* OpenPGP (XEP-0027) is a very old encryption method that has some advantages over OTR but should only be used by experts who know what they are doing.
|
||||
|
||||
#### How do I use OpenPGP
|
||||
|
||||
|
@ -269,14 +297,36 @@ To use OpenPGP you have to install the open source app
|
|||
[OpenKeychain](http://www.openkeychain.org) and then long press on the account in
|
||||
manage accounts and choose renew PGP announcement from the contextual menu.
|
||||
|
||||
#### OMEMO is grayed out. What do I do?
|
||||
OMEMO has two requirements: Your server and the server of your contact need to support PEP. Both of you can verify that individually by opening your account details and selecting ```Server info``` from the menu. The appearing table should list PEP as available. The second requirement is mutual presence subscription. You can verify that by opening the contact details and see if both check boxes *Send presence updates* and *Receive presence updates* are checked.
|
||||
|
||||
#### How does the encryption for conferences work?
|
||||
|
||||
For conferences the only supported encryption method is OpenPGP (OTR does not
|
||||
work with multiple participants). Every participant has to announce their
|
||||
OpenPGP key (see answer above). If you would like to send encrypted messages to
|
||||
a conference you have to make sure that you have every participant's public key
|
||||
in your OpenKeychain. Right now there is no check in Conversations to ensure
|
||||
that. You have to take care of that yourself. Go to the conference details and
|
||||
For conferences only OMEMO and OpenPGP are supported as encryption method. (OTR
|
||||
does not work with multiple participants).
|
||||
|
||||
##### OMEMO
|
||||
|
||||
OMEMO encryption works only in private (members only) conferences that are non-anonymous.
|
||||
You need to have presence subscription with every member of the conference.
|
||||
You can verify that by going into the conference details, long press every member and start
|
||||
a conversation with them. (Or select 'contact details' if they are already in your contact
|
||||
list)
|
||||
|
||||
The owner of a conference can make a public conference private by going into the conference
|
||||
details and hit the settings button (the one with the gears) and select both *private* and
|
||||
*members only*.
|
||||
|
||||
If OMEMO is grayed out long pressing the lock icon will reveal some quick hints on why OMEMO
|
||||
is disabled.
|
||||
|
||||
##### OpenPGP
|
||||
|
||||
Every participant has to announce their OpenPGP key (see answer above).
|
||||
If you would like to send encrypted messages to a conference you have to make
|
||||
sure that you have every participant's public key in your OpenKeychain.
|
||||
Right now there is no check in Conversations to ensure that.
|
||||
You have to take care of that yourself. Go to the conference details and
|
||||
touch every key id (The hexadecimal number below a contact). This will send you
|
||||
to OpenKeychain which will assist you on adding the key. This works best in
|
||||
very small conferences with contacts you are already using OpenPGP with. This
|
||||
|
@ -284,6 +334,23 @@ feature is regarded experimental. Conversations is the only client that uses
|
|||
XEP-0027 with conferences. (The XEP neither specifically allows nor disallows
|
||||
this.)
|
||||
|
||||
#### Why is Conversations not end-to-end encrypted by default
|
||||
We briefly had OMEMO as the default E2EE but it turned out to be a usability nightmare and thus we reverted that. You can find more information in [the commit message](https://github.com/siacs/Conversations/commit/035d0c79572d5981c53d1bff7f30b484c6542f17) of that change.
|
||||
|
||||
Quick reminder that Conversations **always** uses TLS to connect to your server. It won‘t even connect to a server without TLS.
|
||||
|
||||
#### What is Blind Trust Before Verification / why are messages marked with a red lock?
|
||||
|
||||
Read more about the concept on https://gultsch.de/trust.html
|
||||
|
||||
### What clients do I use on other platforms
|
||||
There are XMPP Clients available for all major platforms.
|
||||
####Windows / Linux
|
||||
For your desktop computer we recommend that you use [Gajim](https://gajim.org). You need to install the plugins `OMEMO`, `HTTP Upload` and `URL image preview` to get the best compatibility with Conversations. Plugins can be installed from within the app.
|
||||
####iOS
|
||||
Unfortunately we don‘t have a recommendation for iPhones right now. There are two clients available [ChatSecure](https://chatsecure.org/) and [Monal](https://monal.im/). Both with their own pros and cons.
|
||||
|
||||
|
||||
### Development
|
||||
|
||||
<a name="beta"></a>
|
||||
|
@ -296,16 +363,18 @@ to sign up for the beta test.
|
|||
|
||||
#### How do I build Conversations
|
||||
|
||||
Make sure to have ANDROID_HOME point to your Android SDK
|
||||
Make sure to have ANDROID_HOME point to your Android SDK. Use the Android SDK Manager to install missing dependencies.
|
||||
|
||||
git clone https://github.com/siacs/Conversations.git
|
||||
cd Conversations
|
||||
./gradlew build
|
||||
./gradlew assembleFreeDebug
|
||||
|
||||
There are two build flavors available. *free* and *playstore*. Unless you know what you are doing you only need *free*.
|
||||
|
||||
|
||||
[![Build Status](https://travis-ci.org/siacs/Conversations.svg?branch=development)](https://travis-ci.org/siacs/Conversations)
|
||||
|
||||
### How do I update/add external libraries?
|
||||
#### How do I update/add external libraries?
|
||||
|
||||
If the library you want to update is in Maven Central or JCenter (or has its own
|
||||
Maven repo), add it or update its version in `build.gradle`. If the library is
|
||||
|
@ -328,16 +397,27 @@ To add a new dependency to the `libs/` directory (replacing "name", "branch" and
|
|||
|
||||
If something goes wrong Conversations usually exposes very little information in
|
||||
the UI (other than the fact that something didn't work). However with adb
|
||||
(android debug bridge) you squeeze some more information out of Conversations.
|
||||
(android debug bridge) you can squeeze some more information out of Conversations.
|
||||
These information are especially useful if you are experiencing trouble with
|
||||
your connection or with file transfer.
|
||||
|
||||
To use adb you have to connect your mobile phone to your computer with an USB cable
|
||||
and install `adb`. Most Linux systems have prebuilt packages for that tool. On
|
||||
Debian/Ubuntu for example it is called `android-tools-adb`.
|
||||
|
||||
Furthermore you might have to enable 'USB debugging' in the Developer options of your
|
||||
phone. After that you can just execute the following on your computer:
|
||||
|
||||
adb -d logcat -v time -s conversations
|
||||
|
||||
If need be there are also some Apps on the PlayStore that can be used to show the logcat
|
||||
directly on your rooted phone. (Search for logcat). However in regards to further processing
|
||||
(for example to create an issue here on Github) it is more convenient to just use your PC.
|
||||
|
||||
#### I found a bug
|
||||
|
||||
Please report it to our [issue tracker][issues]. If your app crashes please
|
||||
provide a stack trace. If you are experiencing misbehaviour please provide
|
||||
provide a stack trace. If you are experiencing misbehavior please provide
|
||||
detailed steps to reproduce. Always mention whether you are running the latest
|
||||
Play Store version or the current HEAD. If you are having problems connecting to
|
||||
your XMPP server your file transfer doesn’t work as expected please always
|
||||
|
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
54
art/ic_notifications_none_white80.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_notifications_none_white80.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1543"
|
||||
inkscape:window-height="1093"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="12"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:1;opacity:0.8" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
54
art/ic_notifications_off_white80.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_notifications_off_white80.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1244"
|
||||
inkscape:window-height="936"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="12"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M20 18.69L7.84 6.14 5.27 3.49 4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72l-1-1.03zM12 22c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2zm6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.68z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:1;opacity:0.8" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
54
art/ic_notifications_paused_white80.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_notifications_paused_white80.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1375"
|
||||
inkscape:window-height="999"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="12"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.93 6 11v5l-2 2v1h16v-1l-2-2zm-3.5-6.2l-2.8 3.4h2.8V15h-5v-1.8l2.8-3.4H9.5V8h5v1.8z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:1;opacity:0.8" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
54
art/ic_notifications_white80.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg32"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_notifications_white80.svg">
|
||||
<metadata
|
||||
id="metadata40">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs38" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1471"
|
||||
inkscape:window-height="985"
|
||||
id="namedview36"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="12"
|
||||
inkscape:cy="12"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg32" />
|
||||
<path
|
||||
d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"
|
||||
id="path34"
|
||||
style="fill:#ffffff;fill-opacity:1;opacity:0.8" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
54
art/ic_send_cancel_offline_white.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_send_cancel_offline_white.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1080"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="-36.305085"
|
||||
inkscape:cy="23.898305"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm10 27.17L31.17 34 24 26.83 16.83 34 14 31.17 21.17 24 14 16.83 16.83 14 24 21.17 31.17 14 34 16.83 26.83 24 34 31.17z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:0.627451" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
54
art/ic_send_location_offline_white.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_send_location_offline_white.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="956"
|
||||
inkscape:window-height="1056"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="-36.305085"
|
||||
inkscape:cy="23.898305"
|
||||
inkscape:window-x="2880"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M24 4c-7.73 0-14 6.27-14 14 0 10.5 14 26 14 26s14-15.5 14-26c0-7.73-6.27-14-14-14zm0 19c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:0.627451" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
60
art/ic_send_photo_offline_white.svg
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_send_photo_offline_white.svg">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="956"
|
||||
inkscape:window-height="567"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="10.5688"
|
||||
inkscape:cy="23.898305"
|
||||
inkscape:window-x="960"
|
||||
inkscape:window-y="609"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<circle
|
||||
cx="24"
|
||||
cy="24"
|
||||
r="6.4"
|
||||
id="circle4"
|
||||
style="fill:#ffffff;fill-opacity:0.627451" />
|
||||
<path
|
||||
d="M18 4l-3.66 4H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4h-6.34L30 4H18zm6 30c-5.52 0-10-4.48-10-10s4.48-10 10-10 10 4.48 10 10-4.48 10-10 10z"
|
||||
id="path6"
|
||||
style="fill:#ffffff;fill-opacity:0.627451" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
55
art/ic_send_picture_offline_white.svg
Normal file
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_send_picture_offline_white.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="2560"
|
||||
inkscape:window-height="1392"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="-21.864407"
|
||||
inkscape:cy="23.898305"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M42 38V10c0-2.21-1.79-4-4-4H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4zM17 27l5 6.01L29 24l9 12H10l7-9z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:0.627451" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
70
art/ic_send_text_offline_white.svg
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
id="svg3621"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
width="96"
|
||||
height="96"
|
||||
sodipodi:docname="ic_send_text_offline_white.svg"
|
||||
inkscape:export-filename="/home/daniel/workspace/Conversations/res/drawable-xxhdpi/ic_action_send_now_online.png"
|
||||
inkscape:export-xdpi="154.28572"
|
||||
inkscape:export-ydpi="154.28572">
|
||||
<metadata
|
||||
id="metadata3627">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs3625" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1344"
|
||||
inkscape:window-height="1056"
|
||||
id="namedview3623"
|
||||
showgrid="true"
|
||||
showguides="true"
|
||||
inkscape:zoom="8"
|
||||
inkscape:cx="31.783303"
|
||||
inkscape:cy="56.698828"
|
||||
inkscape:window-x="2880"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg3621"
|
||||
inkscape:snap-others="false">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid3631" />
|
||||
</sodipodi:namedview>
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:0.627451;stroke:none"
|
||||
d="M 3.887575,4.1549246 90.999747,47.676331 3.887575,91.286663 13.203552,52.344101 63.012683,47.720794 13.203552,43.008558 Z"
|
||||
id="path3633"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccc"
|
||||
inkscape:export-filename="/home/daniel/workspace/Conversations/res/drawable-mdpi/ic_action_send_now_dnd.png"
|
||||
inkscape:export-xdpi="51.42857"
|
||||
inkscape:export-ydpi="51.42857" />
|
||||
</svg>
|
After Width: | Height: | Size: 2.3 KiB |
54
art/ic_send_voice_offline_white.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_send_voice_offline_white.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1516"
|
||||
inkscape:window-height="1056"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="-36.711864"
|
||||
inkscape:cy="24"
|
||||
inkscape:window-x="2880"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M24 30c3.31 0 5.98-2.69 5.98-6L30 12c0-3.32-2.68-6-6-6-3.31 0-6 2.68-6 6v12c0 3.31 2.69 6 6 6zm10.6-6c0 6-5.07 10.2-10.6 10.2-5.52 0-10.6-4.2-10.6-10.2H10c0 6.83 5.44 12.47 12 13.44V44h4v-6.56c6.56-.97 12-6.61 12-13.44h-3.4z"
|
||||
id="path4"
|
||||
style="fill:#ffffff;fill-opacity:0.627451" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
54
art/ic_verified_fingerprint.svg
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="ic_verified_fingerprint.svg">
|
||||
<metadata
|
||||
id="metadata10">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs8" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1156"
|
||||
id="namedview6"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="-3.3559322"
|
||||
inkscape:cy="24"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M24 2L6 10v12c0 11.11 7.67 21.47 18 24 10.33-2.53 18-12.89 18-24V10L24 2zm-4 32l-8-8 2.83-2.83L20 28.34l13.17-13.17L36 18 20 34z"
|
||||
id="path4"
|
||||
style="fill:#259b24;fill-opacity:0.87" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.6 KiB |
1
art/main_logo.svg
Symbolic link
|
@ -0,0 +1 @@
|
|||
ic_launcher.svg
|
165
art/message_bubble_received_dark.svg
Normal file
|
@ -0,0 +1,165 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="36"
|
||||
height="26"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
sodipodi:docname="message_bubble_received.svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<filter
|
||||
x="-0.25"
|
||||
y="-0.25"
|
||||
width="1.5"
|
||||
height="1.5"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter3811"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood
|
||||
flood-opacity="0.25"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood3813" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite3815" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
result="blur"
|
||||
id="feGaussianBlur3817" />
|
||||
<feOffset
|
||||
dx="0"
|
||||
dy="1"
|
||||
result="offset"
|
||||
id="feOffset3819" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite3821" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="25.745257"
|
||||
inkscape:cy="9.618802"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:window-width="989"
|
||||
inkscape:window-height="755"
|
||||
inkscape:window-x="22"
|
||||
inkscape:window-y="16"
|
||||
inkscape:window-maximized="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
guidecolor="#000000"
|
||||
guideopacity="0.49803922">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid2985"
|
||||
empspacing="4"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true"
|
||||
spacingx="1px"
|
||||
spacingy="1px"
|
||||
originx="0px"
|
||||
originy="0px"
|
||||
color="#0000ff"
|
||||
opacity="0.03137255" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="20,26"
|
||||
id="guide3060" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="24,26"
|
||||
id="guide3062" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,22"
|
||||
id="guide3064" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,6"
|
||||
id="guide3066" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="26,0"
|
||||
id="guide3068" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="18,0"
|
||||
id="guide3070" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,10"
|
||||
id="guide3074" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,8"
|
||||
id="guide3076" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer"
|
||||
transform="translate(0,-2)">
|
||||
<g
|
||||
id="g3759"
|
||||
style="fill:#326130;fill-opacity:1;stroke:none;fill-rule:nonzero;filter:url(#filter3811)">
|
||||
<path
|
||||
style="display:none"
|
||||
d="m 8,6 c 2,2 4,6 4,10 L 16,6 z"
|
||||
id="path3805"
|
||||
inkscape:connector-curvature="0"
|
||||
transform="translate(0,2)"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path2989"
|
||||
d="M 4,4 16,16 16,4 z"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<rect
|
||||
ry="2"
|
||||
y="4"
|
||||
x="12"
|
||||
height="20"
|
||||
width="20"
|
||||
id="rect2987" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.2 KiB |
167
art/message_bubble_received_grey.svg
Normal file
|
@ -0,0 +1,167 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="36"
|
||||
height="26"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="message_bubble_received_grey.svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<filter
|
||||
x="-0.25"
|
||||
y="-0.25"
|
||||
width="1.5"
|
||||
height="1.5"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter3811"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood
|
||||
flood-opacity="0.25"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood3813" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite3815" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
result="blur"
|
||||
id="feGaussianBlur3817" />
|
||||
<feOffset
|
||||
dx="0"
|
||||
dy="1"
|
||||
result="offset"
|
||||
id="feOffset3819" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite3821" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="-9.879743"
|
||||
inkscape:cy="9.618802"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer"
|
||||
showgrid="true"
|
||||
inkscape:window-width="2135"
|
||||
inkscape:window-height="911"
|
||||
inkscape:window-x="22"
|
||||
inkscape:window-y="16"
|
||||
inkscape:window-maximized="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
guidecolor="#000000"
|
||||
guideopacity="0.49803922">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid2985"
|
||||
empspacing="4"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true"
|
||||
spacingx="1px"
|
||||
spacingy="1px"
|
||||
originx="0px"
|
||||
originy="0px"
|
||||
color="#0000ff"
|
||||
opacity="0.03137255" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="20,26"
|
||||
id="guide3060" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="24,26"
|
||||
id="guide3062" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,22"
|
||||
id="guide3064" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,6"
|
||||
id="guide3066" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="26,0"
|
||||
id="guide3068" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="18,0"
|
||||
id="guide3070" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,10"
|
||||
id="guide3074" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,8"
|
||||
id="guide3076" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer"
|
||||
transform="translate(0,-2)">
|
||||
<g
|
||||
id="g3759"
|
||||
style="fill:#424242;fill-opacity:1;stroke:none;fill-rule:nonzero;filter:url(#filter3811)">
|
||||
<path
|
||||
style="display:none;fill:#424242;fill-opacity:1"
|
||||
d="m 8,6 c 2,2 4,6 4,10 L 16,6 z"
|
||||
id="path3805"
|
||||
inkscape:connector-curvature="0"
|
||||
transform="translate(0,2)"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path2989"
|
||||
d="M 4,4 16,16 16,4 z"
|
||||
sodipodi:nodetypes="cccc"
|
||||
style="fill:#424242;fill-opacity:1" />
|
||||
<rect
|
||||
ry="2"
|
||||
y="4"
|
||||
x="12"
|
||||
height="20"
|
||||
width="20"
|
||||
id="rect2987"
|
||||
style="fill:#424242;fill-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
167
art/message_bubble_sent_grey.svg
Normal file
|
@ -0,0 +1,167 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="36"
|
||||
height="26"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="message_bubble_sent_grey.svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<filter
|
||||
x="-0.25"
|
||||
y="-0.25"
|
||||
width="1.5"
|
||||
height="1.5"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter3811"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood
|
||||
flood-opacity="0.25"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood3813" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite3815" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
result="blur"
|
||||
id="feGaussianBlur3817" />
|
||||
<feOffset
|
||||
dx="0"
|
||||
dy="1"
|
||||
result="offset"
|
||||
id="feOffset3819" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite3821" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="6.244862"
|
||||
inkscape:cy="16.118802"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer"
|
||||
showgrid="true"
|
||||
inkscape:window-width="1554"
|
||||
inkscape:window-height="900"
|
||||
inkscape:window-x="878"
|
||||
inkscape:window-y="369"
|
||||
inkscape:window-maximized="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
guidecolor="#404040"
|
||||
guideopacity="0.49803922">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid2985"
|
||||
empspacing="4"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true"
|
||||
spacingx="1px"
|
||||
spacingy="1px"
|
||||
originx="0px"
|
||||
originy="0px"
|
||||
color="#0000ff"
|
||||
opacity="0.03137255" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="12,26"
|
||||
id="guide3146" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="16,26"
|
||||
id="guide3148" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,22"
|
||||
id="guide3150" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,6"
|
||||
id="guide3152" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="18,0"
|
||||
id="guide3154" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="10,0"
|
||||
id="guide3160" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,20"
|
||||
id="guide3162" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,18"
|
||||
id="guide3164" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer"
|
||||
transform="translate(0,-2)">
|
||||
<g
|
||||
id="g3759"
|
||||
style="fill:#424242;fill-opacity:1;stroke:none;fill-rule:nonzero;filter:url(#filter3811)">
|
||||
<path
|
||||
style="display:none;fill:#424242;fill-opacity:1"
|
||||
d="M 28,18 C 26,16 24,12 24,8 l -4,10 z"
|
||||
id="path3809"
|
||||
inkscape:connector-curvature="0"
|
||||
transform="translate(0,2)"
|
||||
sodipodi:nodetypes="cccc" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path2989"
|
||||
d="m 20,12 0,12 12,0 z"
|
||||
sodipodi:nodetypes="cccc"
|
||||
style="fill:#424242;fill-opacity:1" />
|
||||
<rect
|
||||
ry="2"
|
||||
y="4"
|
||||
x="4"
|
||||
height="20"
|
||||
width="20"
|
||||
id="rect2987"
|
||||
style="fill:#424242;fill-opacity:1" />
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.3 KiB |
68
art/play_gif.svg
Normal file
|
@ -0,0 +1,68 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="play_gif.svg">
|
||||
<metadata
|
||||
id="metadata14">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1200"
|
||||
id="namedview12"
|
||||
showgrid="false"
|
||||
inkscape:zoom="9.8333333"
|
||||
inkscape:cx="1.5762712"
|
||||
inkscape:cy="11.084746"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<defs
|
||||
id="defs4">
|
||||
<path
|
||||
id="a"
|
||||
d="M24 24H0V0h24v24z" />
|
||||
</defs>
|
||||
<clipPath
|
||||
id="b">
|
||||
<use
|
||||
xlink:href="#a"
|
||||
overflow="visible"
|
||||
id="use8" />
|
||||
</clipPath>
|
||||
<path
|
||||
d="M11.5 9H13v6h-1.5zM9 9H6c-.6 0-1 .5-1 1v4c0 .5.4 1 1 1h3c.6 0 1-.5 1-1v-2H8.5v1.5h-2v-3H10V10c0-.5-.4-1-1-1zm10 1.5V9h-4.5v6H16v-2h2v-1.5h-2v-1z"
|
||||
clip-path="url(#b)"
|
||||
id="path10"
|
||||
style="fill:#ffffff;fill-opacity:0.7019608" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
59
art/play_video.svg
Normal file
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="48"
|
||||
height="48"
|
||||
viewBox="0 0 48 48"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.91 r13725"
|
||||
sodipodi:docname="play_video.svg">
|
||||
<metadata
|
||||
id="metadata12">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs10" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1916"
|
||||
inkscape:window-height="1156"
|
||||
id="namedview8"
|
||||
showgrid="false"
|
||||
inkscape:zoom="4.9166667"
|
||||
inkscape:cx="0.91525424"
|
||||
inkscape:cy="24"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="20"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="svg2" />
|
||||
<path
|
||||
d="M0 0h48v48H0z"
|
||||
fill="none"
|
||||
id="path4" />
|
||||
<path
|
||||
d="M20 33l12-9-12-9v18zm4-29C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z"
|
||||
id="path6"
|
||||
style="fill:#ffffff;fill-opacity:0.7019608;opacity:1;stroke:none;stroke-opacity:0.38039216" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.8 KiB |
|
@ -11,42 +11,59 @@ resolutions = {
|
|||
}
|
||||
|
||||
images = {
|
||||
'conversations_baloon.svg' => ['ic_launcher', 48],
|
||||
'ic_launcher.svg' => ['ic_launcher', 48],
|
||||
'main_logo.svg' => ['main_logo', 200],
|
||||
'play_video.svg' => ['play_video', 128],
|
||||
'play_gif.svg' => ['play_gif', 128],
|
||||
'conversations_mono.svg' => ['ic_notification', 24],
|
||||
'ic_received_indicator.svg' => ['ic_received_indicator', 12],
|
||||
'ic_send_text_offline.svg' => ['ic_send_text_offline', 36],
|
||||
'ic_send_text_offline_white.svg' => ['ic_send_text_offline_white', 36],
|
||||
'ic_send_text_online.svg' => ['ic_send_text_online', 36],
|
||||
'ic_send_text_away.svg' => ['ic_send_text_away', 36],
|
||||
'ic_send_text_dnd.svg' => ['ic_send_text_dnd', 36],
|
||||
'ic_send_photo_online.svg' => ['ic_send_photo_online', 36],
|
||||
'ic_send_photo_offline.svg' => ['ic_send_photo_offline', 36],
|
||||
'ic_send_photo_offline_white.svg' => ['ic_send_photo_offline_white', 36],
|
||||
'ic_send_photo_away.svg' => ['ic_send_photo_away', 36],
|
||||
'ic_send_photo_dnd.svg' => ['ic_send_photo_dnd', 36],
|
||||
'ic_send_location_online.svg' => ['ic_send_location_online', 36],
|
||||
'ic_send_location_offline.svg' => ['ic_send_location_offline', 36],
|
||||
'ic_send_location_offline_white.svg' => ['ic_send_location_offline_white', 36],
|
||||
'ic_send_location_away.svg' => ['ic_send_location_away', 36],
|
||||
'ic_send_location_dnd.svg' => ['ic_send_location_dnd', 36],
|
||||
'ic_send_voice_online.svg' => ['ic_send_voice_online', 36],
|
||||
'ic_send_voice_offline.svg' => ['ic_send_voice_offline', 36],
|
||||
'ic_send_voice_offline_white.svg' => ['ic_send_voice_offline_white', 36],
|
||||
'ic_send_voice_away.svg' => ['ic_send_voice_away', 36],
|
||||
'ic_send_voice_dnd.svg' => ['ic_send_voice_dnd', 36],
|
||||
'ic_send_cancel_online.svg' => ['ic_send_cancel_online', 36],
|
||||
'ic_send_cancel_offline.svg' => ['ic_send_cancel_offline', 36],
|
||||
'ic_send_cancel_offline_white.svg' => ['ic_send_cancel_offline_white', 36],
|
||||
'ic_send_cancel_away.svg' => ['ic_send_cancel_away', 36],
|
||||
'ic_send_cancel_dnd.svg' => ['ic_send_cancel_dnd', 36],
|
||||
'ic_send_picture_online.svg' => ['ic_send_picture_online', 36],
|
||||
'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36],
|
||||
'ic_send_picture_offline_white.svg' => ['ic_send_picture_offline_white', 36],
|
||||
'ic_send_picture_away.svg' => ['ic_send_picture_away', 36],
|
||||
'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36],
|
||||
'ic_notifications_none_white80.svg' => ['ic_notifications_none_white80', 24],
|
||||
'ic_notifications_off_white80.svg' => ['ic_notifications_off_white80', 24],
|
||||
'ic_notifications_paused_white80.svg' => ['ic_notifications_paused_white80', 24],
|
||||
'ic_notifications_white80.svg' => ['ic_notifications_white80', 24],
|
||||
'ic_verified_fingerprint.svg' => ['ic_verified_fingerprint', 36],
|
||||
'md_switch_thumb_disable.svg' => ['switch_thumb_disable', 48],
|
||||
'md_switch_thumb_off_normal.svg' => ['switch_thumb_off_normal', 48],
|
||||
'md_switch_thumb_off_pressed.svg' => ['switch_thumb_off_pressed', 48],
|
||||
'md_switch_thumb_on_normal.svg' => ['switch_thumb_on_normal', 48],
|
||||
'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48],
|
||||
'message_bubble_received.svg' => ['message_bubble_received.9', 0],
|
||||
'message_bubble_received_grey.svg' => ['message_bubble_received_grey.9', 0],
|
||||
'message_bubble_received_dark.svg' => ['message_bubble_received_dark.9', 0],
|
||||
'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0],
|
||||
'message_bubble_received_white.svg' => ['message_bubble_received_white.9', 0],
|
||||
'message_bubble_sent.svg' => ['message_bubble_sent.9', 0],
|
||||
'message_bubble_sent_grey.svg' => ['message_bubble_sent_grey.9', 0],
|
||||
}
|
||||
|
||||
# Executable paths for Mac OSX
|
||||
|
|
189
build.gradle
|
@ -1,119 +1,118 @@
|
|||
// Top-level build file where you can add configuration options common to all
|
||||
// sub-projects/modules.
|
||||
buildscript {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.3.1'
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'http://lorenzo.villani.me/android-cropimage/'
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
repositories {
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations {
|
||||
playstoreCompile
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile project(':libs:MemorizingTrustManager')
|
||||
compile 'org.sufficientlysecure:openpgp-api:10.0'
|
||||
compile 'com.soundcloud.android:android-crop:1.0.1@aar'
|
||||
compile 'com.android.support:support-v13:23.0.1'
|
||||
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
|
||||
compile 'org.bouncycastle:bcmail-jdk15on:1.52'
|
||||
compile 'org.jitsi:org.otr4j:0.22'
|
||||
compile 'org.gnu.inet:libidn:1.15'
|
||||
compile 'com.google.zxing:core:3.2.1'
|
||||
compile 'com.google.zxing:android-integration:3.2.1'
|
||||
compile 'de.measite.minidns:minidns:0.1.7'
|
||||
compile 'de.timroes.android:EnhancedListView:0.3.4'
|
||||
compile 'me.leolin:ShortcutBadger:1.1.3@aar'
|
||||
compile 'com.kyleduo.switchbutton:library:1.2.8'
|
||||
compile 'org.whispersystems:axolotl-android:1.3.4'
|
||||
compile 'com.makeramen:roundedimageview:2.2.0'
|
||||
compile project(':libs:MemorizingTrustManager')
|
||||
playstoreCompile 'com.google.android.gms:play-services-gcm:10.0.1'
|
||||
compile 'org.sufficientlysecure:openpgp-api:10.0'
|
||||
compile 'com.soundcloud.android:android-crop:1.0.1@aar'
|
||||
compile 'com.android.support:support-v13:25.1.0'
|
||||
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
|
||||
compile 'org.bouncycastle:bcmail-jdk15on:1.52'
|
||||
compile 'org.jitsi:org.otr4j:0.22'
|
||||
compile 'org.gnu.inet:libidn:1.15'
|
||||
compile 'com.google.zxing:core:3.2.1'
|
||||
compile 'com.google.zxing:android-integration:3.2.1'
|
||||
compile 'de.measite.minidns:minidns:0.1.7'
|
||||
compile 'de.timroes.android:EnhancedListView:0.3.4'
|
||||
compile 'me.leolin:ShortcutBadger:1.1.11@aar'
|
||||
compile 'com.kyleduo.switchbutton:library:1.2.8'
|
||||
compile 'org.whispersystems:axolotl-android:1.3.4'
|
||||
compile 'com.makeramen:roundedimageview:2.2.0'
|
||||
compile "com.wefika:flowlayout:0.4.1"
|
||||
compile 'net.ypresto.androidtranscoder:android-transcoder:0.2.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
travisBuild = System.getenv("TRAVIS") == "true"
|
||||
// allows for -Dpre-dex=false to be set
|
||||
preDexEnabled = "true".equals(System.getProperty("pre-dex", "true"))
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.2"
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 23
|
||||
versionCode 122
|
||||
versionName "1.9.3"
|
||||
project.ext.set(archivesBaseName, archivesBaseName + "-" + versionName);
|
||||
}
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 25
|
||||
versionCode 193
|
||||
versionName "1.15.5"
|
||||
archivesBaseName += "-$versionName"
|
||||
applicationId "eu.siacs.conversations"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
dexOptions {
|
||||
// Skip pre-dexing when running on Travis CI or when disabled via -Dpre-dex=false.
|
||||
preDexLibraries = preDexEnabled && !travisBuild
|
||||
}
|
||||
|
||||
//
|
||||
// To sign release builds, create the file `gradle.properties` in
|
||||
// $HOME/.gradle or in your project directory with this content:
|
||||
//
|
||||
// mStoreFile=/path/to/key.store
|
||||
// mStorePassword=xxx
|
||||
// mKeyAlias=alias
|
||||
// mKeyPassword=xxx
|
||||
//
|
||||
if (project.hasProperty('mStoreFile') &&
|
||||
project.hasProperty('mStorePassword') &&
|
||||
project.hasProperty('mKeyAlias') &&
|
||||
project.hasProperty('mKeyPassword')) {
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file(mStoreFile)
|
||||
storePassword mStorePassword
|
||||
keyAlias mKeyAlias
|
||||
keyPassword mKeyPassword
|
||||
}
|
||||
}
|
||||
buildTypes.release.signingConfig = signingConfigs.release
|
||||
} else {
|
||||
buildTypes.release.signingConfig = null
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_7
|
||||
targetCompatibility JavaVersion.VERSION_1_7
|
||||
}
|
||||
|
||||
applicationVariants.all { variant ->
|
||||
if (variant.name.equals('release')) {
|
||||
variant.outputs.each { output ->
|
||||
if (output.zipAlign != null) {
|
||||
output.zipAlign.outputFile = new File(output.outputFile.parent, rootProject.name + "-${variant.versionName}.apk")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
productFlavors {
|
||||
playstore
|
||||
free
|
||||
}
|
||||
if (project.hasProperty('mStoreFile') &&
|
||||
project.hasProperty('mStorePassword') &&
|
||||
project.hasProperty('mKeyAlias') &&
|
||||
project.hasProperty('mKeyPassword')) {
|
||||
signingConfigs {
|
||||
release {
|
||||
storeFile file(mStoreFile)
|
||||
storePassword mStorePassword
|
||||
keyAlias mKeyAlias
|
||||
keyPassword mKeyPassword
|
||||
}
|
||||
}
|
||||
buildTypes.release.signingConfig = signingConfigs.release
|
||||
} else {
|
||||
buildTypes.release.signingConfig = null
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource'
|
||||
}
|
||||
lintOptions {
|
||||
disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource'
|
||||
}
|
||||
|
||||
subprojects {
|
||||
subprojects {
|
||||
|
||||
afterEvaluate {
|
||||
if (getPlugins().hasPlugin('android') ||
|
||||
getPlugins().hasPlugin('android-library')) {
|
||||
afterEvaluate {
|
||||
if (getPlugins().hasPlugin('android') ||
|
||||
getPlugins().hasPlugin('android-library')) {
|
||||
|
||||
configure(android.lintOptions) {
|
||||
disable 'AndroidGradlePluginVersion', 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
configure(android.lintOptions) {
|
||||
disable 'AndroidGradlePluginVersion', 'MissingTranslation'
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
exclude 'META-INF/BCKEY.DSA'
|
||||
exclude 'META-INF/BCKEY.SF'
|
||||
}
|
||||
}
|
||||
|
|
12
docs/XEPs.md
|
@ -2,13 +2,17 @@
|
|||
* XEP-0030: Service Discovery
|
||||
* XEP-0045: Multi-User Chat
|
||||
* XEP-0048: Bookmarks
|
||||
* XEP-0084: User Avatar
|
||||
* XEP-0085: Chat State Notifications
|
||||
* XEP-0092: Software Version
|
||||
* XEP-0115: Entity Capabilities
|
||||
* XEP-0163: Personal Eventing Protocol (avatars and nicks)
|
||||
* XEP-0166: Jingle (only used for file transfer)
|
||||
* XEP-0172: User Nickname
|
||||
* XEP-0184: Message Delivery Receipts (reply only)
|
||||
* XEP-0191: Blocking command
|
||||
* XEP-0198: Stream Management
|
||||
* XEP-0199: XMPP Ping
|
||||
* XEP-0234: Jingle File Transfer
|
||||
* XEP-0237: Roster Versioning
|
||||
* XEP-0245: The /me Command
|
||||
|
@ -16,7 +20,13 @@
|
|||
* XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
|
||||
* XEP-0261: Jingle In-Band Bytestreams Transport Method
|
||||
* XEP-0280: Message Carbons
|
||||
* XEP-0308: Last Message Correction
|
||||
* XEP-0313: Message Archive Management
|
||||
* XEP-0319: Last User Interaction in Presence
|
||||
* XEP-0333: Chat Markers
|
||||
* XEP-0352: Client State Indication
|
||||
* XEP-0191: Blocking command
|
||||
* XEP-0357: Push Notifications
|
||||
* XEP-0363: HTTP File Upload
|
||||
* XEP-0368: SRV records for XMPP over TLS
|
||||
* XEP-0377: Spam Reporting
|
||||
* XEP-0384: OMEMO Encryption
|
||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
#Sat Nov 22 17:47:57 CET 2014
|
||||
#Sat Apr 09 13:15:52 CEST 2016
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip
|
||||
|
|
|
@ -3,18 +3,18 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:1.2.3'
|
||||
classpath 'com.android.tools.build:gradle:2.0.0'
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'android-library'
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 19
|
||||
buildToolsVersion "19.1"
|
||||
compileSdkVersion 25
|
||||
buildToolsVersion "25.0.2"
|
||||
defaultConfig {
|
||||
minSdkVersion 7
|
||||
targetSdkVersion 19
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 25
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
|
|
|
@ -35,15 +35,33 @@ import android.app.PendingIntent;
|
|||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.util.SparseArray;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.*;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.text.SimpleDateFormat;
|
||||
|
@ -51,8 +69,10 @@ import java.util.Collection;
|
|||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import javax.net.ssl.HostnameVerifier;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.TrustManager;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
@ -68,7 +88,15 @@ import javax.net.ssl.X509TrustManager;
|
|||
* <b>WARNING:</b> This only works if a dedicated thread is used for
|
||||
* opening sockets!
|
||||
*/
|
||||
public class MemorizingTrustManager implements X509TrustManager {
|
||||
public class MemorizingTrustManager {
|
||||
|
||||
|
||||
private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||
private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||
private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z");
|
||||
private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z");
|
||||
private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z");
|
||||
|
||||
final static String DECISION_INTENT = "de.duenndns.ssl.DECISION";
|
||||
final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId";
|
||||
final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert";
|
||||
|
@ -94,6 +122,7 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
private KeyStore appKeyStore;
|
||||
private X509TrustManager defaultTrustManager;
|
||||
private X509TrustManager appTrustManager;
|
||||
private String poshCacheDir;
|
||||
|
||||
/** Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager.
|
||||
*
|
||||
|
@ -149,28 +178,11 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE);
|
||||
keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE);
|
||||
|
||||
poshCacheDir = app.getFilesDir().getAbsolutePath()+"/posh_cache/";
|
||||
|
||||
appKeyStore = loadAppKeyStore();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a X509TrustManager list containing a new instance of
|
||||
* TrustManagerFactory.
|
||||
*
|
||||
* This function is meant for convenience only. You can use it
|
||||
* as follows to integrate TrustManagerFactory for HTTPS sockets:
|
||||
*
|
||||
* <pre>
|
||||
* SSLContext sc = SSLContext.getInstance("TLS");
|
||||
* sc.init(null, MemorizingTrustManager.getInstanceList(this),
|
||||
* new java.security.SecureRandom());
|
||||
* HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
|
||||
* </pre>
|
||||
* @param c Activity or Service to show the Dialog / Notification
|
||||
*/
|
||||
public static X509TrustManager[] getInstanceList(Context c) {
|
||||
return new X509TrustManager[] { new MemorizingTrustManager(c) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds an Activity to the MTM for displaying the query dialog.
|
||||
|
@ -389,7 +401,7 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
return false;
|
||||
}
|
||||
|
||||
public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer, boolean interactive)
|
||||
public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive)
|
||||
throws CertificateException
|
||||
{
|
||||
LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")");
|
||||
|
@ -419,6 +431,15 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
else
|
||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
||||
} catch (CertificateException e) {
|
||||
boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false);
|
||||
if (domain != null && isServer && trustSystemCAs && !isIp(domain)) {
|
||||
String hash = getBase64Hash(chain[0],"SHA-256");
|
||||
List<String> fingerprints = getPoshFingerprints(domain);
|
||||
if (hash != null && fingerprints.contains(hash)) {
|
||||
Log.d("mtm","trusted cert fingerprint of "+domain+" via posh");
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.printStackTrace();
|
||||
if (interactive) {
|
||||
interactCert(chain, authType, e);
|
||||
|
@ -429,20 +450,147 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
}
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||
throws CertificateException
|
||||
{
|
||||
checkCertTrusted(chain, authType, false,true);
|
||||
private List<String> getPoshFingerprints(String domain) {
|
||||
List<String> cached = getPoshFingerprintsFromCache(domain);
|
||||
if (cached == null) {
|
||||
return getPoshFingerprintsFromServer(domain);
|
||||
} else {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||
throws CertificateException
|
||||
{
|
||||
checkCertTrusted(chain, authType, true,true);
|
||||
private List<String> getPoshFingerprintsFromServer(String domain) {
|
||||
return getPoshFingerprintsFromServer(domain, "https://"+domain+"/.well-known/posh/xmpp-client.json",-1,true);
|
||||
}
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers()
|
||||
{
|
||||
private List<String> getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) {
|
||||
Log.d("mtm","downloading json for "+domain+" from "+url);
|
||||
try {
|
||||
List<String> results = new ArrayList<>();
|
||||
HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.setReadTimeout(5000);
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
|
||||
String inputLine;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
while ((inputLine = in.readLine()) != null) {
|
||||
builder.append(inputLine);
|
||||
}
|
||||
JSONObject jsonObject = new JSONObject(builder.toString());
|
||||
in.close();
|
||||
int expires = jsonObject.getInt("expires");
|
||||
if (expires <= 0) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
if (maxTtl >= 0) {
|
||||
expires = Math.min(maxTtl,expires);
|
||||
}
|
||||
String redirect;
|
||||
try {
|
||||
redirect = jsonObject.getString("url");
|
||||
} catch (JSONException e) {
|
||||
redirect = null;
|
||||
}
|
||||
if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) {
|
||||
return getPoshFingerprintsFromServer(domain, redirect, expires, false);
|
||||
}
|
||||
JSONArray fingerprints = jsonObject.getJSONArray("fingerprints");
|
||||
for(int i = 0; i < fingerprints.length(); i++) {
|
||||
JSONObject fingerprint = fingerprints.getJSONObject(i);
|
||||
String sha256 = fingerprint.getString("sha-256");
|
||||
if (sha256 != null) {
|
||||
results.add(sha256);
|
||||
}
|
||||
}
|
||||
writeFingerprintsToCache(domain, results,1000L * expires+System.currentTimeMillis());
|
||||
return results;
|
||||
} catch (Exception e) {
|
||||
Log.d("mtm","error fetching posh "+e.getMessage());
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
private File getPoshCacheFile(String domain) {
|
||||
return new File(poshCacheDir+domain+".json");
|
||||
}
|
||||
|
||||
private void writeFingerprintsToCache(String domain, List<String> results, long expires) {
|
||||
File file = getPoshCacheFile(domain);
|
||||
file.getParentFile().mkdirs();
|
||||
try {
|
||||
file.createNewFile();
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("expires",expires);
|
||||
jsonObject.put("fingerprints",new JSONArray(results));
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
outputStream.write(jsonObject.toString().getBytes());
|
||||
outputStream.flush();
|
||||
outputStream.close();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private List<String> getPoshFingerprintsFromCache(String domain) {
|
||||
File file = getPoshCacheFile(domain);
|
||||
try {
|
||||
InputStream is = new FileInputStream(file);
|
||||
BufferedReader buf = new BufferedReader(new InputStreamReader(is));
|
||||
|
||||
String line = buf.readLine();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
while(line != null){
|
||||
sb.append(line).append("\n");
|
||||
line = buf.readLine();
|
||||
}
|
||||
JSONObject jsonObject = new JSONObject(sb.toString());
|
||||
is.close();
|
||||
long expires = jsonObject.getLong("expires");
|
||||
long expiresIn = expires - System.currentTimeMillis();
|
||||
if (expiresIn < 0) {
|
||||
file.delete();
|
||||
return null;
|
||||
} else {
|
||||
Log.d("mtm","posh fingerprints expire in "+(expiresIn/1000)+"s");
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
JSONArray jsonArray = jsonObject.getJSONArray("fingerprints");
|
||||
for(int i = 0; i < jsonArray.length(); ++i) {
|
||||
result.add(jsonArray.getString(i));
|
||||
}
|
||||
return result;
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} catch (JSONException e) {
|
||||
file.delete();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isIp(final String server) {
|
||||
return server != null && (
|
||||
PATTERN_IPV4.matcher(server).matches()
|
||||
|| PATTERN_IPV6.matcher(server).matches()
|
||||
|| PATTERN_IPV6_6HEX4DEC.matcher(server).matches()
|
||||
|| PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches()
|
||||
|| PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches());
|
||||
}
|
||||
|
||||
private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException {
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance(digest);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
}
|
||||
md.update(certificate.getEncoded());
|
||||
return Base64.encodeToString(md.digest(),Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
private X509Certificate[] getAcceptedIssuers() {
|
||||
LOGGER.log(Level.FINE, "getAcceptedIssuers()");
|
||||
return defaultTrustManager.getAcceptedIssuers();
|
||||
}
|
||||
|
@ -553,22 +701,6 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
certDetails(si, cert);
|
||||
return si.toString();
|
||||
}
|
||||
|
||||
// We can use Notification.Builder once MTM's minSDK is >= 11
|
||||
@SuppressWarnings("deprecation")
|
||||
void startActivityNotification(Intent intent, int decisionId, String certName) {
|
||||
Notification n = new Notification(android.R.drawable.ic_lock_lock,
|
||||
master.getString(R.string.mtm_notification),
|
||||
System.currentTimeMillis());
|
||||
PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0);
|
||||
n.setLatestEventInfo(master.getApplicationContext(),
|
||||
master.getString(R.string.mtm_notification),
|
||||
certName, call);
|
||||
n.flags |= Notification.FLAG_AUTO_CANCEL;
|
||||
|
||||
notificationManager.notify(NOTIFICATION_ID + decisionId, n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the top-most entry of the activity stack.
|
||||
*
|
||||
|
@ -598,7 +730,6 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
getUI().startActivity(ni);
|
||||
} catch (Exception e) {
|
||||
LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e);
|
||||
startActivityNotification(ni, myId, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -702,28 +833,45 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
}
|
||||
@Override
|
||||
public boolean verify(String hostname, SSLSession session) {
|
||||
return verify(hostname, session, true);
|
||||
return verify(hostname, session, false);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
public X509TrustManager getNonInteractive(String domain) {
|
||||
return new NonInteractiveMemorizingTrustManager(domain);
|
||||
}
|
||||
|
||||
public X509TrustManager getInteractive(String domain) {
|
||||
return new InteractiveMemorizingTrustManager(domain);
|
||||
}
|
||||
|
||||
public X509TrustManager getNonInteractive() {
|
||||
return new NonInteractiveMemorizingTrustManager();
|
||||
return new NonInteractiveMemorizingTrustManager(null);
|
||||
}
|
||||
|
||||
public X509TrustManager getInteractive() {
|
||||
return new InteractiveMemorizingTrustManager(null);
|
||||
}
|
||||
|
||||
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
|
||||
|
||||
private final String domain;
|
||||
|
||||
public NonInteractiveMemorizingTrustManager(String domain) {
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
||||
throws CertificateException {
|
||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||
throws CertificateException {
|
||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false);
|
||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -732,4 +880,28 @@ public class MemorizingTrustManager implements X509TrustManager {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
private class InteractiveMemorizingTrustManager implements X509TrustManager {
|
||||
private final String domain;
|
||||
|
||||
public InteractiveMemorizingTrustManager(String domain) {
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
|
||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
||||
throws CertificateException {
|
||||
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
return MemorizingTrustManager.this.getAcceptedIssuers();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
# To enable ProGuard in your project, edit project.properties
|
||||
# to define the proguard.config property as described in that file.
|
||||
#
|
||||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in ${sdk.dir}/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the ProGuard
|
||||
# include property in project.properties.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# 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 *;
|
||||
#}
|
|
@ -1,27 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /home/sam/android-sdk-linux/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the ProGuard
|
||||
# include property in project.properties.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# 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 *;
|
||||
#}
|
||||
|
||||
-dontwarn javax.naming.**
|
||||
|
||||
-keep class * extends java.util.ListResourceBundle {
|
||||
protected Object[][] getContents();
|
||||
}
|
||||
|
||||
-keepnames class * implements android.os.Parcelable {
|
||||
public static final ** CREATOR;
|
||||
}
|
BIN
screenshots.png
Before Width: | Height: | Size: 1,011 KiB After Width: | Height: | Size: 195 KiB |
|
@ -0,0 +1,28 @@
|
|||
package eu.siacs.conversations.services;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
|
||||
public class PushManagementService {
|
||||
|
||||
protected final XmppConnectionService mXmppConnectionService;
|
||||
|
||||
public PushManagementService(XmppConnectionService service) {
|
||||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
public void registerPushTokenOnServer(Account account) {
|
||||
//stub implementation. only affects playstore flavor
|
||||
}
|
||||
|
||||
public boolean available(Account account) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isStub() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean availableAndUseful(Account account) {
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,24 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
package="eu.siacs.conversations"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="eu.siacs.conversations">
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.VIBRATE"/>
|
||||
<uses-permission android:name="android.permission.NFC"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_PROFILE" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_PHONE_STATE"
|
||||
tools:node="remove"/>
|
||||
tools:node="remove" />
|
||||
|
||||
<uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
|
@ -26,14 +27,14 @@
|
|||
android:label="@string/app_name"
|
||||
android:theme="@style/ConversationsTheme"
|
||||
tools:replace="android:label">
|
||||
<service android:name=".services.XmppConnectionService"/>
|
||||
<service android:name=".services.XmppConnectionService" />
|
||||
|
||||
<receiver android:name=".services.EventReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED"/>
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
|
||||
<action android:name="android.intent.action.ACTION_SHUTDOWN"/>
|
||||
<action android:name="android.media.RINGER_MODE_CHANGED"/>
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
|
||||
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
|
||||
<action android:name="android.media.RINGER_MODE_CHANGED" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
@ -41,102 +42,130 @@
|
|||
android:name=".ui.ConversationActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"
|
||||
android:minWidth="300dp"
|
||||
android:minHeight="300dp"
|
||||
android:windowSoftInputMode="stateHidden">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.StartConversationActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:label="@string/title_activity_start_conversation"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SENDTO"/>
|
||||
<action android:name="android.intent.action.SENDTO" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="imto"/>
|
||||
<data android:host="jabber"/>
|
||||
<data android:scheme="imto" />
|
||||
<data android:host="jabber" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="xmpp"/>
|
||||
<data android:scheme="xmpp" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:scheme="xmpp"/>
|
||||
<data android:scheme="xmpp" />
|
||||
</intent-filter>
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:host="conversations.im" />
|
||||
<data android:pathPrefix="/i/" />
|
||||
<data android:pathPrefix="/j/" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.WelcomeActivity"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleTask"/>
|
||||
<activity
|
||||
android:name=".ui.MagicCreateActivity"
|
||||
android:label="@string/create_account"
|
||||
android:launchMode="singleTask"/>
|
||||
<activity
|
||||
android:name=".ui.SetPresenceActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:label="@string/change_presence"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize" />
|
||||
<activity
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:label="@string/title_activity_settings"/>
|
||||
android:label="@string/title_activity_settings" />
|
||||
<activity
|
||||
android:name=".ui.ChooseContactActivity"
|
||||
android:label="@string/title_activity_choose_contact"/>
|
||||
android:label="@string/title_activity_choose_contact" />
|
||||
<activity
|
||||
android:name=".ui.BlocklistActivity"
|
||||
android:label="@string/title_activity_block_list"/>
|
||||
android:label="@string/title_activity_block_list" />
|
||||
<activity
|
||||
android:name=".ui.ChangePasswordActivity"
|
||||
android:label="@string/change_password_on_server"/>
|
||||
android:label="@string/change_password_on_server" />
|
||||
<activity
|
||||
android:name=".ui.ManageAccountActivity"
|
||||
android:label="@string/title_activity_manage_accounts"
|
||||
android:launchMode="singleTask"/>
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.EditAccountActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:windowSoftInputMode="stateHidden|adjustResize"/>
|
||||
android:windowSoftInputMode="stateHidden|adjustResize" />
|
||||
<activity
|
||||
android:name=".ui.ConferenceDetailsActivity"
|
||||
android:label="@string/title_activity_conference_details"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name=".ui.ContactDetailsActivity"
|
||||
android:label="@string/title_activity_contact_details"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name=".ui.PublishProfilePictureActivity"
|
||||
android:label="@string/mgmt_account_publish_avatar"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name=".ui.VerifyOTRActivity"
|
||||
android:label="@string/verify_otr"
|
||||
android:windowSoftInputMode="stateHidden"/>
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name=".ui.ShareWithActivity"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="text/plain"/>
|
||||
<data android:mimeType="text/plain" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND"/>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="*/*"/>
|
||||
<data android:mimeType="*/*" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE"/>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="image/*"/>
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value=".services.ContactChooserTargetService" />
|
||||
|
@ -144,27 +173,46 @@
|
|||
<activity
|
||||
android:name=".ui.TrustKeysActivity"
|
||||
android:label="@string/trust_omemo_fingerprints"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"/>
|
||||
android:windowSoftInputMode="stateAlwaysHidden" />
|
||||
<activity
|
||||
android:name="de.duenndns.ssl.MemorizingActivity"
|
||||
android:theme="@style/ConversationsTheme"
|
||||
tools:replace="android:theme"/>
|
||||
tools:replace="android:theme" />
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:parentActivityName=".ui.SettingsActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="eu.siacs.conversations.ui.SettingsActivity"/>
|
||||
android:value="eu.siacs.conversations.ui.SettingsActivity" />
|
||||
</activity>
|
||||
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
|
||||
<service android:name=".services.ExportLogsService"/>
|
||||
<service android:name=".services.ContactChooserTargetService"
|
||||
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
|
||||
|
||||
<service android:name=".services.ExportLogsService" />
|
||||
<service
|
||||
android:name=".services.ContactChooserTargetService"
|
||||
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.chooser.ChooserTargetService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<provider
|
||||
android:authorities="${applicationId}.barcodes"
|
||||
android:name=".services.BarcodeProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true"/>
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -6,29 +6,59 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
|||
|
||||
public final class Config {
|
||||
|
||||
|
||||
private static final int UNENCRYPTED = 1;
|
||||
private static final int OPENPGP = 2;
|
||||
private static final int OTR = 4;
|
||||
private static final int OMEMO = 8;
|
||||
|
||||
private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO;
|
||||
|
||||
public static boolean supportUnencrypted() {
|
||||
return (ENCRYPTION_MASK & UNENCRYPTED) != 0;
|
||||
}
|
||||
|
||||
public static boolean supportOpenPgp() {
|
||||
return (ENCRYPTION_MASK & OPENPGP) != 0;
|
||||
}
|
||||
|
||||
public static boolean supportOtr() {
|
||||
return (ENCRYPTION_MASK & OTR) != 0;
|
||||
}
|
||||
|
||||
public static boolean supportOmemo() {
|
||||
return (ENCRYPTION_MASK & OMEMO) != 0;
|
||||
}
|
||||
|
||||
public static boolean multipleEncryptionChoices() {
|
||||
return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
|
||||
}
|
||||
|
||||
public static final String LOGTAG = "conversations";
|
||||
|
||||
public static final String BUG_REPORTS = "bugs@conversations.im";
|
||||
|
||||
|
||||
public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
|
||||
public static final String MAGIC_CREATE_DOMAIN = "conversations.im";
|
||||
public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
|
||||
public static final boolean HIDE_PGP_IN_UI = false; //some more consumer focused clients might want to disable OpenPGP
|
||||
public static final boolean FORCE_E2E_ENCRYPTION = false; //disables ability to send unencrypted 1-on-1
|
||||
|
||||
public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; //very dangerous. you should have a good reason to set this to true
|
||||
public static final boolean FORCE_ORBOT = false; // always use TOR
|
||||
public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false;
|
||||
public static final boolean SHOW_CONNECTED_ACCOUNTS = false; //show number of connected accounts in foreground notification
|
||||
public static final boolean SHOW_DISABLE_FOREGROUND = false; //if set to true the foreground notification has a button to disable it
|
||||
|
||||
public static final boolean ALWAYS_NOTIFY_BY_DEFAULT = false;
|
||||
|
||||
public static final boolean LEGACY_NAMESPACE_HTTP_UPLOAD = false;
|
||||
|
||||
public static final int PING_MAX_INTERVAL = 300;
|
||||
public static final int IDLE_PING_INTERVAL = 600; //540 is minimum according to docs;
|
||||
public static final int PING_MIN_INTERVAL = 30;
|
||||
public static final int LOW_PING_TIMEOUT = 1; // used after push received
|
||||
public static final int PING_TIMEOUT = 15;
|
||||
public static final int SOCKET_TIMEOUT = 15;
|
||||
public static final int CONNECT_TIMEOUT = 90;
|
||||
public static final int CONNECT_DISCO_TIMEOUT = 20;
|
||||
public static final int CARBON_GRACE_PERIOD = 90;
|
||||
public static final int MINI_GRACE_PERIOD = 750;
|
||||
|
||||
public static final int AVATAR_SIZE = 192;
|
||||
|
@ -46,29 +76,46 @@ public final class Config {
|
|||
|
||||
public static final int REFRESH_UI_INTERVAL = 500;
|
||||
|
||||
public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096;
|
||||
|
||||
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
public static final long OMEMO_AUTO_EXPIRY = 7 * MILLISECONDS_IN_DAY;
|
||||
public static final boolean REMOVE_BROKEN_DEVICES = false;
|
||||
public static final boolean OMEMO_PADDING = false;
|
||||
public static boolean PUT_AUTH_TAG_INTO_KEY = false;
|
||||
|
||||
|
||||
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
|
||||
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
||||
public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance
|
||||
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
|
||||
public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background
|
||||
public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption
|
||||
|
||||
public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
|
||||
|
||||
public static final boolean REPORT_WRONG_FILESIZE_IN_OTR_JINGLE = true;
|
||||
|
||||
public static final boolean SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON = false;
|
||||
|
||||
public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
|
||||
|
||||
public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments
|
||||
|
||||
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
|
||||
|
||||
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
public static final boolean PARSE_REAL_JID_FROM_MUC_MAM = false; //dangerous if server doesn’t filter
|
||||
|
||||
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
|
||||
public static final int MAM_MAX_MESSAGES = 500;
|
||||
|
||||
public static final long FREQUENT_RESTARTS_DETECTION_WINDOW = 12 * 60 * 60 * 1000; // 10 hours
|
||||
public static final long FREQUENT_RESTARTS_THRESHOLD = 0; // previous value was 16;
|
||||
|
||||
public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
|
||||
public static final int TYPING_TIMEOUT = 8;
|
||||
|
||||
public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
public static final String ENABLED_CIPHERS[] = {
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
|
||||
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",
|
||||
|
@ -107,6 +154,5 @@ public final class Config {
|
|||
};
|
||||
|
||||
private Config() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,28 +53,30 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
|
|||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
private KeyPair loadKey(JSONObject keys) {
|
||||
private KeyPair loadKey(final JSONObject keys) {
|
||||
if (keys == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
|
||||
BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
|
||||
BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
|
||||
BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
|
||||
BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
|
||||
DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
|
||||
DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
|
||||
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
return null;
|
||||
synchronized (keys) {
|
||||
try {
|
||||
BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
|
||||
BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
|
||||
BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
|
||||
BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
|
||||
BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
|
||||
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
|
||||
DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
|
||||
DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
|
||||
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
} catch (InvalidKeySpecException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -122,7 +124,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
|
|||
|
||||
@Override
|
||||
public String getFallbackMessage(SessionID arg0) {
|
||||
return "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that";
|
||||
return MessageGenerator.OTR_FALLBACK_MESSAGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -192,8 +194,9 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
|
|||
} catch (final InvalidJidException ignored) {
|
||||
|
||||
}
|
||||
|
||||
packet.setType(MessagePacket.TYPE_CHAT);
|
||||
packet.addChild("encryption","urn:xmpp:eme:0")
|
||||
.setAttribute("namespace","urn:xmpp:otr:0");
|
||||
account.getXmppConnection().sendMessagePacket(packet);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,162 +1,219 @@
|
|||
package eu.siacs.conversations.crypto;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.UiCallback;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
|
||||
public class PgpDecryptionService {
|
||||
|
||||
private final XmppConnectionService xmppConnectionService;
|
||||
private final ConcurrentHashMap<String, List<Message>> messages = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, Boolean> decryptingMessages = new ConcurrentHashMap<>();
|
||||
private Boolean keychainLocked = false;
|
||||
private final Object keychainLockedLock = new Object();
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
private OpenPgpApi openPgpApi = null;
|
||||
|
||||
public PgpDecryptionService(XmppConnectionService xmppConnectionService) {
|
||||
this.xmppConnectionService = xmppConnectionService;
|
||||
protected final ArrayDeque<Message> messages = new ArrayDeque();
|
||||
protected final HashSet<Message> pendingNotifications = new HashSet<>();
|
||||
Message currentMessage;
|
||||
private PendingIntent pendingIntent;
|
||||
|
||||
|
||||
public PgpDecryptionService(XmppConnectionService service) {
|
||||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
public synchronized boolean decrypt(final Message message, boolean notify) {
|
||||
messages.add(message);
|
||||
if (notify && pendingIntent == null) {
|
||||
pendingNotifications.add(message);
|
||||
continueDecryption();
|
||||
return false;
|
||||
} else {
|
||||
continueDecryption();
|
||||
return notify;
|
||||
}
|
||||
}
|
||||
|
||||
public void add(Message message) {
|
||||
if (isRunning()) {
|
||||
decryptDirectly(message);
|
||||
} else {
|
||||
store(message);
|
||||
public synchronized void decrypt(final List<Message> list) {
|
||||
for(Message message : list) {
|
||||
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
continueDecryption();
|
||||
}
|
||||
|
||||
public synchronized void discard(List<Message> discards) {
|
||||
this.messages.removeAll(discards);
|
||||
this.pendingNotifications.removeAll(discards);
|
||||
}
|
||||
|
||||
public synchronized void discard(Message message) {
|
||||
this.messages.remove(message);
|
||||
this.pendingNotifications.remove(message);
|
||||
}
|
||||
|
||||
protected synchronized void decryptNext() {
|
||||
if (pendingIntent == null
|
||||
&& getOpenPgpApi() != null
|
||||
&& (currentMessage = messages.poll()) != null) {
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
executeApi(currentMessage);
|
||||
decryptNext();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
public void addAll(List<Message> messagesList) {
|
||||
if (!messagesList.isEmpty()) {
|
||||
String conversationUuid = messagesList.get(0).getConversation().getUuid();
|
||||
if (!messages.containsKey(conversationUuid)) {
|
||||
List<Message> list = Collections.synchronizedList(new LinkedList<Message>());
|
||||
messages.put(conversationUuid, list);
|
||||
}
|
||||
synchronized (messages.get(conversationUuid)) {
|
||||
messages.get(conversationUuid).addAll(messagesList);
|
||||
}
|
||||
decryptAllMessages();
|
||||
}
|
||||
}
|
||||
public synchronized void continueDecryption(boolean resetPending) {
|
||||
if (resetPending) {
|
||||
this.pendingIntent = null;
|
||||
}
|
||||
continueDecryption();
|
||||
}
|
||||
|
||||
public void onKeychainUnlocked() {
|
||||
synchronized (keychainLockedLock) {
|
||||
keychainLocked = false;
|
||||
}
|
||||
decryptAllMessages();
|
||||
}
|
||||
public synchronized void continueDecryption() {
|
||||
if (currentMessage == null) {
|
||||
decryptNext();
|
||||
}
|
||||
}
|
||||
|
||||
public void onKeychainLocked() {
|
||||
synchronized (keychainLockedLock) {
|
||||
keychainLocked = true;
|
||||
}
|
||||
xmppConnectionService.updateConversationUi();
|
||||
}
|
||||
private synchronized OpenPgpApi getOpenPgpApi() {
|
||||
if (openPgpApi == null) {
|
||||
this.openPgpApi = mXmppConnectionService.getOpenPgpApi();
|
||||
}
|
||||
return this.openPgpApi;
|
||||
}
|
||||
|
||||
public void onOpenPgpServiceBound() {
|
||||
decryptAllMessages();
|
||||
}
|
||||
private void executeApi(Message message) {
|
||||
synchronized (message) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
|
||||
if (message.getType() == Message.TYPE_TEXT) {
|
||||
InputStream is = new ByteArrayInputStream(message.getBody().getBytes());
|
||||
final OutputStream os = new ByteArrayOutputStream();
|
||||
Intent result = getOpenPgpApi().executeApi(params, is, os);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
try {
|
||||
os.flush();
|
||||
final String body = os.toString();
|
||||
if (body == null) {
|
||||
throw new IOException("body was null");
|
||||
}
|
||||
message.setBody(body);
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
|
||||
if (message.trusted()
|
||||
&& message.treatAsDownloadable() != Message.Decision.NEVER
|
||||
&& manager.getAutoAcceptFileSize() > 0) {
|
||||
manager.createNewDownloadConnection(message);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
|
||||
}
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
synchronized (PgpDecryptionService.this) {
|
||||
PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
messages.addFirst(message);
|
||||
currentMessage = null;
|
||||
storePendingIntent(pendingIntent);
|
||||
}
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
break;
|
||||
}
|
||||
} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
|
||||
try {
|
||||
final DownloadableFile inputFile = mXmppConnectionService.getFileBackend().getFile(message, false);
|
||||
final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
|
||||
outputFile.getParentFile().mkdirs();
|
||||
outputFile.createNewFile();
|
||||
InputStream is = new FileInputStream(inputFile);
|
||||
OutputStream os = new FileOutputStream(outputFile);
|
||||
Intent result = getOpenPgpApi().executeApi(params, is, os);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
URL url = message.getFileParams().url;
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
inputFile.delete();
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(outputFile);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
synchronized (PgpDecryptionService.this) {
|
||||
PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
messages.addFirst(message);
|
||||
currentMessage = null;
|
||||
storePendingIntent(pendingIntent);
|
||||
}
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
break;
|
||||
}
|
||||
} catch (final IOException e) {
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyIfPending(message);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
synchronized (keychainLockedLock) {
|
||||
return !keychainLocked;
|
||||
}
|
||||
}
|
||||
private synchronized void notifyIfPending(Message message) {
|
||||
if (pendingNotifications.remove(message)) {
|
||||
mXmppConnectionService.getNotificationService().push(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void store(Message message) {
|
||||
if (messages.containsKey(message.getConversation().getUuid())) {
|
||||
messages.get(message.getConversation().getUuid()).add(message);
|
||||
} else {
|
||||
List<Message> messageList = Collections.synchronizedList(new LinkedList<Message>());
|
||||
messageList.add(message);
|
||||
messages.put(message.getConversation().getUuid(), messageList);
|
||||
}
|
||||
}
|
||||
private void storePendingIntent(PendingIntent pendingIntent) {
|
||||
this.pendingIntent = pendingIntent;
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
|
||||
private void decryptAllMessages() {
|
||||
for (String uuid : messages.keySet()) {
|
||||
decryptMessages(uuid);
|
||||
}
|
||||
}
|
||||
public synchronized boolean hasPendingIntent(Conversation conversation) {
|
||||
if (pendingIntent == null) {
|
||||
return false;
|
||||
} else {
|
||||
for(Message message : messages) {
|
||||
if (message.getConversation() == conversation) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void decryptMessages(final String uuid) {
|
||||
synchronized (decryptingMessages) {
|
||||
Boolean decrypting = decryptingMessages.get(uuid);
|
||||
if ((decrypting != null && !decrypting) || decrypting == null) {
|
||||
decryptingMessages.put(uuid, true);
|
||||
decryptMessage(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
public PendingIntent getPendingIntent() {
|
||||
return pendingIntent;
|
||||
}
|
||||
|
||||
private void decryptMessage(final String uuid) {
|
||||
Message message = null;
|
||||
synchronized (messages.get(uuid)) {
|
||||
while (!messages.get(uuid).isEmpty()) {
|
||||
if (messages.get(uuid).get(0).getEncryption() == Message.ENCRYPTION_PGP) {
|
||||
if (isRunning()) {
|
||||
message = messages.get(uuid).remove(0);
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
messages.get(uuid).remove(0);
|
||||
}
|
||||
}
|
||||
if (message != null && xmppConnectionService.getPgpEngine() != null) {
|
||||
xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
|
||||
|
||||
@Override
|
||||
public void userInputRequried(PendingIntent pi, Message message) {
|
||||
messages.get(uuid).add(0, message);
|
||||
decryptingMessages.put(uuid, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success(Message message) {
|
||||
xmppConnectionService.updateConversationUi();
|
||||
decryptMessage(uuid);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int error, Message message) {
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
|
||||
xmppConnectionService.updateConversationUi();
|
||||
decryptMessage(uuid);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
decryptingMessages.put(uuid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void decryptDirectly(final Message message) {
|
||||
if (message.getEncryption() == Message.ENCRYPTION_PGP && xmppConnectionService.getPgpEngine() != null) {
|
||||
xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
|
||||
|
||||
@Override
|
||||
public void userInputRequried(PendingIntent pi, Message message) {
|
||||
store(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success(Message message) {
|
||||
xmppConnectionService.updateConversationUi();
|
||||
xmppConnectionService.getNotificationService().updateNotification(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int error, Message message) {
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
|
||||
xmppConnectionService.updateConversationUi();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
public boolean isConnected() {
|
||||
return getOpenPgpApi() != null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,9 @@ package eu.siacs.conversations.crypto;
|
|||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
|
||||
|
@ -15,15 +16,14 @@ import java.io.FileOutputStream;
|
|||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URL;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.UiCallback;
|
||||
|
@ -37,99 +37,6 @@ public class PgpEngine {
|
|||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
public void decrypt(final Message message,
|
||||
final UiCallback<Message> callback) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
|
||||
if (message.getType() == Message.TYPE_TEXT) {
|
||||
InputStream is = new ByteArrayInputStream(message.getBody()
|
||||
.getBytes());
|
||||
final OutputStream os = new ByteArrayOutputStream();
|
||||
api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
|
||||
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
|
||||
OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
try {
|
||||
os.flush();
|
||||
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||
message.setBody(os.toString());
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
|
||||
if (message.trusted()
|
||||
&& message.treatAsDownloadable() != Message.Decision.NEVER
|
||||
&& manager.getAutoAcceptFileSize() > 0) {
|
||||
manager.createNewDownloadConnection(message);
|
||||
}
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
callback.success(message);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequried((PendingIntent) result
|
||||
.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
|
||||
message);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
|
||||
try {
|
||||
final DownloadableFile inputFile = this.mXmppConnectionService
|
||||
.getFileBackend().getFile(message, false);
|
||||
final DownloadableFile outputFile = this.mXmppConnectionService
|
||||
.getFileBackend().getFile(message, true);
|
||||
outputFile.getParentFile().mkdirs();
|
||||
outputFile.createNewFile();
|
||||
InputStream is = new FileInputStream(inputFile);
|
||||
OutputStream os = new FileOutputStream(outputFile);
|
||||
api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
|
||||
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
|
||||
OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
URL url = message.getFileParams().url;
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message,url);
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
PgpEngine.this.mXmppConnectionService
|
||||
.updateMessage(message);
|
||||
inputFile.delete();
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||
intent.setData(Uri.fromFile(outputFile));
|
||||
mXmppConnectionService.sendBroadcast(intent);
|
||||
callback.success(message);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequried(
|
||||
(PendingIntent) result
|
||||
.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
|
||||
message);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (final IOException e) {
|
||||
callback.error(R.string.error_decrypting_file, message);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void encrypt(final Message message, final UiCallback<Message> callback) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_ENCRYPT);
|
||||
|
@ -158,7 +65,6 @@ public class PgpEngine {
|
|||
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
|
||||
OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
|
@ -185,6 +91,7 @@ public class PgpEngine {
|
|||
message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(conversation.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
break;
|
||||
}
|
||||
|
@ -204,7 +111,6 @@ public class PgpEngine {
|
|||
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
|
||||
OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
|
@ -223,6 +129,7 @@ public class PgpEngine {
|
|||
message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(conversation.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, message);
|
||||
break;
|
||||
}
|
||||
|
@ -259,7 +166,6 @@ public class PgpEngine {
|
|||
InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
Intent result = api.executeApi(params, is, os);
|
||||
notifyPgpDecryptionService(account, OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
|
||||
OpenPgpApi.RESULT_CODE_ERROR)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
|
@ -273,6 +179,7 @@ public class PgpEngine {
|
|||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
return 0;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
|
@ -295,6 +202,7 @@ public class PgpEngine {
|
|||
account);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, account);
|
||||
}
|
||||
}
|
||||
|
@ -303,7 +211,7 @@ public class PgpEngine {
|
|||
|
||||
public void generateSignature(final Account account, String status,
|
||||
final UiCallback<Account> callback) {
|
||||
if (account.getPgpId() == -1) {
|
||||
if (account.getPgpId() == 0) {
|
||||
return;
|
||||
}
|
||||
Intent params = new Intent();
|
||||
|
@ -312,11 +220,11 @@ public class PgpEngine {
|
|||
params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId());
|
||||
InputStream is = new ByteArrayInputStream(status.getBytes());
|
||||
final OutputStream os = new ByteArrayOutputStream();
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": signing status message \""+status+"\"");
|
||||
api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
|
||||
|
||||
@Override
|
||||
public void onReturn(Intent result) {
|
||||
notifyPgpDecryptionService(account, OpenPgpApi.ACTION_SIGN, result);
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
StringBuilder signatureBuilder = new StringBuilder();
|
||||
|
@ -351,7 +259,13 @@ public class PgpEngine {
|
|||
account);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
callback.error(R.string.openpgp_error, account);
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
if (error != null && "signing subkey not found!".equals(error.getMessage())) {
|
||||
callback.error(0,account);
|
||||
} else {
|
||||
logError(account, error);
|
||||
callback.error(R.string.unable_to_connect_to_keychain, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -375,40 +289,30 @@ public class PgpEngine {
|
|||
contact);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(contact.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
callback.error(R.string.openpgp_error, contact);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public PendingIntent getIntentForKey(Contact contact) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_GET_KEY);
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
|
||||
Intent result = api.executeApi(params, null, null);
|
||||
return (PendingIntent) result
|
||||
.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
private static void logError(Account account, OpenPgpError error) {
|
||||
if (error != null) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": OpenKeychain error '"+error.getMessage()+"' code="+error.getErrorId());
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": OpenKeychain error with no message");
|
||||
}
|
||||
}
|
||||
|
||||
public PendingIntent getIntentForKey(Account account, long pgpKeyId) {
|
||||
public PendingIntent getIntentForKey(Contact contact) {
|
||||
return getIntentForKey(contact.getPgpKeyId());
|
||||
}
|
||||
|
||||
public PendingIntent getIntentForKey(long pgpKeyId) {
|
||||
Intent params = new Intent();
|
||||
params.setAction(OpenPgpApi.ACTION_GET_KEY);
|
||||
params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId);
|
||||
Intent result = api.executeApi(params, null, null);
|
||||
return (PendingIntent) result
|
||||
.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
}
|
||||
|
||||
private void notifyPgpDecryptionService(Account account, String action, final Intent result) {
|
||||
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
|
||||
case OpenPgpApi.RESULT_CODE_SUCCESS:
|
||||
if (OpenPgpApi.ACTION_SIGN.equals(action)) {
|
||||
account.getPgpDecryptionService().onKeychainUnlocked();
|
||||
}
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
account.getPgpDecryptionService().onKeychainLocked();
|
||||
break;
|
||||
}
|
||||
return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.security.KeyChain;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
@ -24,13 +25,17 @@ import java.security.PrivateKey;
|
|||
import java.security.Security;
|
||||
import java.security.Signature;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
|
@ -39,6 +44,7 @@ import eu.siacs.conversations.entities.Conversation;
|
|||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.parser.IqParser;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
|
||||
|
@ -51,6 +57,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
|
||||
public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
|
||||
public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
|
||||
public static final String PEP_DEVICE_LIST_NOTIFY = PEP_DEVICE_LIST + "+notify";
|
||||
public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
|
||||
public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification";
|
||||
|
||||
|
@ -70,28 +77,50 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
private int numPublishTriesOnEmptyPep = 0;
|
||||
private boolean pepBroken = false;
|
||||
|
||||
private AtomicBoolean ownPushPending = new AtomicBoolean(false);
|
||||
|
||||
@Override
|
||||
public void onAdvancedStreamFeaturesAvailable(Account account) {
|
||||
if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().pep()) {
|
||||
if (Config.supportOmemo()
|
||||
&& account.getXmppConnection() != null
|
||||
&& account.getXmppConnection().getFeatures().pep()) {
|
||||
publishBundlesIfNeeded(true, false);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization");
|
||||
}
|
||||
}
|
||||
|
||||
public boolean fetchMapHasErrors(Contact contact) {
|
||||
Jid jid = contact.getJid().toBareJid();
|
||||
if (deviceIds.get(jid) != null) {
|
||||
for (Integer foreignId : this.deviceIds.get(jid)) {
|
||||
AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
|
||||
if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
|
||||
return true;
|
||||
public boolean fetchMapHasErrors(List<Jid> jids) {
|
||||
for(Jid jid : jids) {
|
||||
if (deviceIds.get(jid) != null) {
|
||||
for (Integer foreignId : this.deviceIds.get(jid)) {
|
||||
AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId);
|
||||
if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void preVerifyFingerprint(Contact contact, String fingerprint) {
|
||||
axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint);
|
||||
}
|
||||
|
||||
public void preVerifyFingerprint(Account account, String fingerprint) {
|
||||
axolotlStore.preVerifyFingerprint(account, account.getJid().toBareJid().toPreppedString(), fingerprint);
|
||||
}
|
||||
|
||||
public boolean hasVerifiedKeys(String name) {
|
||||
for(XmppAxolotlSession session : this.sessions.getAll(new AxolotlAddress(name,0)).values()) {
|
||||
if (session.getTrust().isVerified()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static class AxolotlAddressMap<T> {
|
||||
protected Map<String, Map<Integer, T>> map;
|
||||
protected final Object MAP_LOCK = new Object();
|
||||
|
@ -158,15 +187,28 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
|
||||
for (Integer deviceId : deviceIds) {
|
||||
AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
|
||||
IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
|
||||
if(Config.X509_VERIFICATION) {
|
||||
X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", ""));
|
||||
if (certificate != null) {
|
||||
Bundle information = CryptoHelper.extractCertificateInformation(certificate);
|
||||
try {
|
||||
final String cn = information.getString("subject_cn");
|
||||
final Jid jid = Jid.fromString(bareJid);
|
||||
Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
|
||||
account.getRoster().getContact(jid).setCommonName(cn);
|
||||
} catch (final InvalidJidException ignored) {
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillMap(SQLiteAxolotlStore store) {
|
||||
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
|
||||
putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
|
||||
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toPreppedString());
|
||||
putDevicesForJid(account.getJid().toBareJid().toPreppedString(), deviceIds, store);
|
||||
for (Contact contact : account.getRoster().getContacts()) {
|
||||
Jid bareJid = contact.getJid().toBareJid();
|
||||
String address = bareJid.toString();
|
||||
|
@ -180,7 +222,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
public void put(AxolotlAddress address, XmppAxolotlSession value) {
|
||||
super.put(address, value);
|
||||
value.setNotFresh();
|
||||
xmppConnectionService.syncRosterToDisk(account);
|
||||
xmppConnectionService.syncRosterToDisk(account); //TODO why?
|
||||
}
|
||||
|
||||
public void put(XmppAxolotlSession session) {
|
||||
|
@ -193,11 +235,26 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
SUCCESS,
|
||||
SUCCESS_VERIFIED,
|
||||
TIMEOUT,
|
||||
SUCCESS_TRUSTED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
|
||||
|
||||
public void clearErrorFor(Jid jid) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, FetchStatus> devices = this.map.get(jid.toBareJid().toPreppedString());
|
||||
if (devices == null) {
|
||||
return;
|
||||
}
|
||||
for(Map.Entry<Integer, FetchStatus> entry : devices.entrySet()) {
|
||||
if (entry.getValue() == FetchStatus.ERROR) {
|
||||
Log.d(Config.LOGTAG,"resetting error for "+jid.toBareJid()+"("+entry.getKey()+")");
|
||||
entry.setValue(FetchStatus.TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLogprefix(Account account) {
|
||||
|
@ -222,57 +279,80 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", "");
|
||||
}
|
||||
|
||||
public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
|
||||
return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
|
||||
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status) {
|
||||
return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), status);
|
||||
}
|
||||
|
||||
public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) {
|
||||
return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust);
|
||||
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, Jid jid) {
|
||||
return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), status);
|
||||
}
|
||||
|
||||
public long getNumTrustedKeys(Contact contact) {
|
||||
return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
|
||||
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, List<Jid> jids) {
|
||||
Set<IdentityKey> keys = new HashSet<>();
|
||||
for(Jid jid : jids) {
|
||||
keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
public long getNumTrustedKeys(Jid jid) {
|
||||
return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString());
|
||||
}
|
||||
|
||||
public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
|
||||
for(Jid jid : jids) {
|
||||
if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private AxolotlAddress getAddressForJid(Jid jid) {
|
||||
return new AxolotlAddress(jid.toString(), 0);
|
||||
return new AxolotlAddress(jid.toPreppedString(), 0);
|
||||
}
|
||||
|
||||
private Set<XmppAxolotlSession> findOwnSessions() {
|
||||
public Collection<XmppAxolotlSession> findOwnSessions() {
|
||||
AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
|
||||
return new HashSet<>(this.sessions.getAll(ownAddress).values());
|
||||
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(ownAddress).values());
|
||||
Collections.sort(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
|
||||
|
||||
|
||||
public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
|
||||
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
|
||||
return new HashSet<>(this.sessions.getAll(contactAddress).values());
|
||||
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress).values());
|
||||
Collections.sort(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
public Set<String> getFingerprintsForOwnSessions() {
|
||||
Set<String> fingerprints = new HashSet<>();
|
||||
for (XmppAxolotlSession session : findOwnSessions()) {
|
||||
fingerprints.add(session.getFingerprint());
|
||||
private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
|
||||
HashSet<XmppAxolotlSession> sessions = new HashSet<>();
|
||||
for(Jid jid : conversation.getAcceptedCryptoTargets()) {
|
||||
sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values());
|
||||
}
|
||||
return fingerprints;
|
||||
return sessions;
|
||||
}
|
||||
|
||||
public Set<String> getFingerprintsForContact(final Contact contact) {
|
||||
Set<String> fingerprints = new HashSet<>();
|
||||
for (XmppAxolotlSession session : findSessionsforContact(contact)) {
|
||||
fingerprints.add(session.getFingerprint());
|
||||
}
|
||||
return fingerprints;
|
||||
}
|
||||
|
||||
private boolean hasAny(Contact contact) {
|
||||
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
|
||||
return sessions.hasAny(contactAddress);
|
||||
private boolean hasAny(Jid jid) {
|
||||
return sessions.hasAny(getAddressForJid(jid));
|
||||
}
|
||||
|
||||
public boolean isPepBroken() {
|
||||
return this.pepBroken;
|
||||
}
|
||||
|
||||
public void resetBrokenness() {
|
||||
this.pepBroken = false;
|
||||
numPublishTriesOnEmptyPep = 0;
|
||||
}
|
||||
|
||||
public void clearErrorsInFetchStatusMap(Jid jid) {
|
||||
fetchStatusMap.clearErrorFor(jid);
|
||||
}
|
||||
|
||||
public void regenerateKeys(boolean wipeOther) {
|
||||
axolotlStore.regenerate();
|
||||
sessions.clear();
|
||||
|
@ -284,62 +364,66 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return axolotlStore.getLocalRegistrationId();
|
||||
}
|
||||
|
||||
public AxolotlAddress getOwnAxolotlAddress() {
|
||||
return new AxolotlAddress(account.getJid().toBareJid().toPreppedString(),getOwnDeviceId());
|
||||
}
|
||||
|
||||
public Set<Integer> getOwnDeviceIds() {
|
||||
return this.deviceIds.get(account.getJid().toBareJid());
|
||||
}
|
||||
|
||||
private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
|
||||
final XmppAxolotlSession.Trust from,
|
||||
final XmppAxolotlSession.Trust to) {
|
||||
for (Integer deviceId : deviceIds) {
|
||||
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
|
||||
XmppAxolotlSession session = sessions.get(address);
|
||||
if (session != null && session.getFingerprint() != null
|
||||
&& session.getTrust() == from) {
|
||||
session.setTrust(to);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
|
||||
if (jid.toBareJid().equals(account.getJid().toBareJid())) {
|
||||
if (!deviceIds.isEmpty()) {
|
||||
Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attemps and pepBroken status.");
|
||||
pepBroken = false;
|
||||
numPublishTriesOnEmptyPep = 0;
|
||||
}
|
||||
if (deviceIds.contains(getOwnDeviceId())) {
|
||||
deviceIds.remove(getOwnDeviceId());
|
||||
} else {
|
||||
publishOwnDeviceId(deviceIds);
|
||||
}
|
||||
for (Integer deviceId : deviceIds) {
|
||||
AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
|
||||
if (sessions.get(ownDeviceAddress) == null) {
|
||||
buildSessionFromPEP(ownDeviceAddress);
|
||||
boolean me = jid.toBareJid().equals(account.getJid().toBareJid());
|
||||
if (me && ownPushPending.getAndSet(false)) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring own device update because of pending push");
|
||||
return;
|
||||
}
|
||||
boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId());
|
||||
if (me) {
|
||||
deviceIds.remove(getOwnDeviceId());
|
||||
}
|
||||
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString()));
|
||||
expiredDevices.removeAll(deviceIds);
|
||||
for (Integer deviceId : expiredDevices) {
|
||||
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
|
||||
XmppAxolotlSession session = sessions.get(address);
|
||||
if (session != null && session.getFingerprint() != null) {
|
||||
if (session.getTrust().isActive()) {
|
||||
session.setTrust(session.getTrust().toInactive());
|
||||
}
|
||||
}
|
||||
}
|
||||
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
|
||||
expiredDevices.removeAll(deviceIds);
|
||||
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
|
||||
XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
|
||||
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED_X509,
|
||||
XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
|
||||
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
|
||||
XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
|
||||
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
|
||||
XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
|
||||
Set<Integer> newDevices = new HashSet<>(deviceIds);
|
||||
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
|
||||
XmppAxolotlSession.Trust.TRUSTED);
|
||||
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509,
|
||||
XmppAxolotlSession.Trust.TRUSTED_X509);
|
||||
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
|
||||
XmppAxolotlSession.Trust.UNDECIDED);
|
||||
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
|
||||
XmppAxolotlSession.Trust.UNTRUSTED);
|
||||
for (Integer deviceId : newDevices) {
|
||||
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
|
||||
XmppAxolotlSession session = sessions.get(address);
|
||||
if (session != null && session.getFingerprint() != null) {
|
||||
if (!session.getTrust().isActive()) {
|
||||
Log.d(Config.LOGTAG,"reactivating device with fingerprint "+session.getFingerprint());
|
||||
session.setTrust(session.getTrust().toActive());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (me) {
|
||||
if (Config.OMEMO_AUTO_EXPIRY != 0) {
|
||||
needsPublishing |= deviceIds.removeAll(getExpiredDevices());
|
||||
}
|
||||
for (Integer deviceId : deviceIds) {
|
||||
AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
|
||||
if (sessions.get(ownDeviceAddress) == null) {
|
||||
FetchStatus status = fetchStatusMap.get(ownDeviceAddress);
|
||||
if (status == null || status == FetchStatus.TIMEOUT) {
|
||||
fetchStatusMap.put(ownDeviceAddress, FetchStatus.PENDING);
|
||||
this.buildSessionFromPEP(ownDeviceAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (needsPublishing) {
|
||||
publishOwnDeviceId(deviceIds);
|
||||
}
|
||||
}
|
||||
this.deviceIds.put(jid, deviceIds);
|
||||
mXmppConnectionService.updateConversationUi(); //update the lock icon
|
||||
mXmppConnectionService.keyStatusUpdated(null);
|
||||
}
|
||||
|
||||
|
@ -352,16 +436,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
deviceIds.add(getOwnDeviceId());
|
||||
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
|
||||
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
|
||||
@Override
|
||||
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||
// TODO: implement this!
|
||||
}
|
||||
});
|
||||
mXmppConnectionService.sendIqPacket(account, publish, null);
|
||||
}
|
||||
|
||||
public void purgeKey(final String fingerprint) {
|
||||
axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
|
||||
public void distrustFingerprint(final String fingerprint) {
|
||||
final String fp = fingerprint.replaceAll("\\s", "");
|
||||
final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp);
|
||||
axolotlStore.setFingerprintStatus(fp,fingerprintStatus.toUntrusted());
|
||||
}
|
||||
|
||||
public void publishOwnDeviceIdIfNeeded() {
|
||||
|
@ -378,41 +459,62 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
} else {
|
||||
Element item = mXmppConnectionService.getIqParser().getItem(packet);
|
||||
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
|
||||
if (!deviceIds.contains(getOwnDeviceId())) {
|
||||
publishOwnDeviceId(deviceIds);
|
||||
}
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved own device list: "+deviceIds);
|
||||
registerDevices(account.getJid().toBareJid(),deviceIds);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void publishOwnDeviceId(Set<Integer> deviceIds) {
|
||||
Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
|
||||
if (!deviceIdsCopy.contains(getOwnDeviceId())) {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist.");
|
||||
if (deviceIdsCopy.isEmpty()) {
|
||||
if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
|
||||
pepBroken = true;
|
||||
return;
|
||||
} else {
|
||||
numPublishTriesOnEmptyPep++;
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
|
||||
}
|
||||
} else {
|
||||
numPublishTriesOnEmptyPep = 0;
|
||||
}
|
||||
deviceIdsCopy.add(getOwnDeviceId());
|
||||
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy);
|
||||
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
|
||||
@Override
|
||||
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||
if (packet.getType() != IqPacket.TYPE.RESULT) {
|
||||
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
|
||||
private Set<Integer> getExpiredDevices() {
|
||||
Set<Integer> devices = new HashSet<>();
|
||||
for(XmppAxolotlSession session : findOwnSessions()) {
|
||||
if (session.getTrust().isActive()) {
|
||||
long diff = System.currentTimeMillis() - session.getTrust().getLastActivation();
|
||||
if (diff > Config.OMEMO_AUTO_EXPIRY) {
|
||||
long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account,session.getFingerprint());
|
||||
long hours = Math.round(lastMessageDiff/(1000*60.0*60.0));
|
||||
if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) {
|
||||
devices.add(session.getRemoteAddress().getDeviceId());
|
||||
session.setTrust(session.getTrust().toInactive());
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": added own device " + session.getFingerprint() + " to list of expired devices. Last message received "+hours+" hours ago");
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": own device "+session.getFingerprint()+" was active "+hours+" hours ago");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
|
||||
public void publishOwnDeviceId(Set<Integer> deviceIds) {
|
||||
Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids");
|
||||
if (deviceIdsCopy.isEmpty()) {
|
||||
if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
|
||||
pepBroken = true;
|
||||
return;
|
||||
} else {
|
||||
numPublishTriesOnEmptyPep++;
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
|
||||
}
|
||||
} else {
|
||||
numPublishTriesOnEmptyPep = 0;
|
||||
}
|
||||
deviceIdsCopy.add(getOwnDeviceId());
|
||||
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy);
|
||||
ownPushPending.set(true);
|
||||
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
|
||||
@Override
|
||||
public void onIqPacketReceived(Account account, IqPacket packet) {
|
||||
ownPushPending.set(false);
|
||||
if (packet.getType() == IqPacket.TYPE.ERROR) {
|
||||
pepBroken = true;
|
||||
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
|
||||
|
@ -571,29 +673,69 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
|
||||
publishOwnDeviceIdIfNeeded();
|
||||
}
|
||||
} else {
|
||||
} else if (packet.getType() == IqPacket.TYPE.ERROR) {
|
||||
pepBroken = true;
|
||||
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error"));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isContactAxolotlCapable(Contact contact) {
|
||||
Jid jid = contact.getJid().toBareJid();
|
||||
return hasAny(contact) ||
|
||||
(deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
|
||||
public enum AxolotlCapability {
|
||||
FULL,
|
||||
MISSING_PRESENCE,
|
||||
MISSING_KEYS,
|
||||
WRONG_CONFIGURATION,
|
||||
NO_MEMBERS
|
||||
}
|
||||
|
||||
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
|
||||
return axolotlStore.getFingerprintTrust(fingerprint);
|
||||
public boolean isConversationAxolotlCapable(Conversation conversation) {
|
||||
return isConversationAxolotlCapableDetailed(conversation).first == AxolotlCapability.FULL;
|
||||
}
|
||||
|
||||
public Pair<AxolotlCapability,Jid> isConversationAxolotlCapableDetailed(Conversation conversation) {
|
||||
if (conversation.getMode() == Conversation.MODE_SINGLE
|
||||
|| (conversation.getMucOptions().membersOnly() && conversation.getMucOptions().nonanonymous())) {
|
||||
final List<Jid> jids = getCryptoTargets(conversation);
|
||||
for(Jid jid : jids) {
|
||||
if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) {
|
||||
if (conversation.getAccount().getRoster().getContact(jid).mutualPresenceSubscription()) {
|
||||
return new Pair<>(AxolotlCapability.MISSING_KEYS,jid);
|
||||
} else {
|
||||
return new Pair<>(AxolotlCapability.MISSING_PRESENCE,jid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (jids.size() > 0) {
|
||||
return new Pair<>(AxolotlCapability.FULL, null);
|
||||
} else {
|
||||
return new Pair<>(AxolotlCapability.NO_MEMBERS, null);
|
||||
}
|
||||
} else {
|
||||
return new Pair<>(AxolotlCapability.WRONG_CONFIGURATION, null);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Jid> getCryptoTargets(Conversation conversation) {
|
||||
final List<Jid> jids;
|
||||
if (conversation.getMode() == Conversation.MODE_SINGLE) {
|
||||
jids = Arrays.asList(conversation.getJid().toBareJid());
|
||||
} else {
|
||||
jids = conversation.getMucOptions().getMembers();
|
||||
}
|
||||
return jids;
|
||||
}
|
||||
|
||||
public FingerprintStatus getFingerprintTrust(String fingerprint) {
|
||||
return axolotlStore.getFingerprintStatus(fingerprint);
|
||||
}
|
||||
|
||||
public X509Certificate getFingerprintCertificate(String fingerprint) {
|
||||
return axolotlStore.getFingerprintCertificate(fingerprint);
|
||||
}
|
||||
|
||||
public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
|
||||
axolotlStore.setFingerprintTrust(fingerprint, trust);
|
||||
public void setFingerprintTrust(String fingerprint, FingerprintStatus status) {
|
||||
axolotlStore.setFingerprintStatus(fingerprint, status);
|
||||
}
|
||||
|
||||
private void verifySessionWithPEP(final XmppAxolotlSession session) {
|
||||
|
@ -616,9 +758,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
|
||||
String fingerprint = session.getFingerprint();
|
||||
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
|
||||
setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509);
|
||||
setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
|
||||
axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
|
||||
fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
|
||||
Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
|
||||
try {
|
||||
final String cn = information.getString("subject_cn");
|
||||
final Jid jid = Jid.fromString(address.getName());
|
||||
Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
|
||||
account.getRoster().getContact(jid).setCommonName(cn);
|
||||
} catch (final InvalidJidException ignored) {
|
||||
//ignored
|
||||
}
|
||||
finishBuildingSessionsFromPEP(address);
|
||||
return;
|
||||
} catch (Exception e) {
|
||||
|
@ -641,25 +792,44 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
private final Set<Integer> PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>();
|
||||
|
||||
private void finishBuildingSessionsFromPEP(final AxolotlAddress address) {
|
||||
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
|
||||
if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
|
||||
&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
|
||||
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0);
|
||||
Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress);
|
||||
Map<Integer, FetchStatus> remote = fetchStatusMap.getAll(address);
|
||||
if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) {
|
||||
FetchStatus report = null;
|
||||
if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED)
|
||||
| fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) {
|
||||
if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) {
|
||||
report = FetchStatus.SUCCESS;
|
||||
} else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) {
|
||||
report = FetchStatus.SUCCESS_VERIFIED;
|
||||
} else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR)
|
||||
|| fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
|
||||
} else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) {
|
||||
report = FetchStatus.SUCCESS_TRUSTED;
|
||||
} else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) {
|
||||
report = FetchStatus.ERROR;
|
||||
}
|
||||
mXmppConnectionService.keyStatusUpdated(report);
|
||||
}
|
||||
if (Config.REMOVE_BROKEN_DEVICES) {
|
||||
Set<Integer> ownDeviceIds = new HashSet<>(getOwnDeviceIds());
|
||||
boolean publish = false;
|
||||
for (Map.Entry<Integer, FetchStatus> entry : own.entrySet()) {
|
||||
int id = entry.getKey();
|
||||
if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) {
|
||||
publish = true;
|
||||
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": error fetching own device with id " + id + ". removing from announcement");
|
||||
}
|
||||
}
|
||||
if (publish) {
|
||||
publishOwnDeviceId(ownDeviceIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void buildSessionFromPEP(final AxolotlAddress address) {
|
||||
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.toString());
|
||||
if (address.getDeviceId() == getOwnDeviceId()) {
|
||||
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString());
|
||||
if (address.equals(getOwnAxolotlAddress())) {
|
||||
throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
|
||||
}
|
||||
|
||||
|
@ -706,7 +876,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
if (Config.X509_VERIFICATION) {
|
||||
verifySessionWithPEP(session);
|
||||
} else {
|
||||
fetchStatusMap.put(address, FetchStatus.SUCCESS);
|
||||
FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s",""));
|
||||
FetchStatus fetchStatus;
|
||||
if (status != null && status.isVerified()) {
|
||||
fetchStatus = FetchStatus.SUCCESS_VERIFIED;
|
||||
} else if (status != null && status.isTrusted()) {
|
||||
fetchStatus = FetchStatus.SUCCESS_TRUSTED;
|
||||
} else {
|
||||
fetchStatus = FetchStatus.SUCCESS;
|
||||
}
|
||||
fetchStatusMap.put(address, fetchStatus);
|
||||
finishBuildingSessionsFromPEP(address);
|
||||
}
|
||||
} catch (UntrustedIdentityException | InvalidKeyException e) {
|
||||
|
@ -728,37 +907,35 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
|
||||
public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
|
||||
return findDevicesWithoutSession(conversation.getContact().getJid().toBareJid());
|
||||
}
|
||||
|
||||
public Set<AxolotlAddress> findDevicesWithoutSession(final Jid contactJid) {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + contactJid);
|
||||
Set<AxolotlAddress> addresses = new HashSet<>();
|
||||
if (deviceIds.get(contactJid) != null) {
|
||||
for (Integer foreignId : this.deviceIds.get(contactJid)) {
|
||||
AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
|
||||
if (sessions.get(address) == null) {
|
||||
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
|
||||
if (identityKey != null) {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
|
||||
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
|
||||
sessions.put(address, session);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + contactJid + ":" + foreignId);
|
||||
if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
|
||||
addresses.add(address);
|
||||
for(Jid jid : getCryptoTargets(conversation)) {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
|
||||
if (deviceIds.get(jid) != null) {
|
||||
for (Integer foreignId : this.deviceIds.get(jid)) {
|
||||
AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId);
|
||||
if (sessions.get(address) == null) {
|
||||
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
|
||||
if (identityKey != null) {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
|
||||
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
|
||||
sessions.put(address, session);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken");
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId);
|
||||
if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
|
||||
addresses.add(address);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
|
||||
}
|
||||
} else {
|
||||
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
|
||||
}
|
||||
if (deviceIds.get(account.getJid().toBareJid()) != null) {
|
||||
for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
|
||||
AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
|
||||
AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownId);
|
||||
if (sessions.get(address) == null) {
|
||||
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
|
||||
if (identityKey != null) {
|
||||
|
@ -802,12 +979,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
|
||||
public boolean trustedSessionVerified(final Conversation conversation) {
|
||||
Set<XmppAxolotlSession> sessions = findSessionsforContact(conversation.getContact());
|
||||
Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation);
|
||||
sessions.addAll(findOwnSessions());
|
||||
boolean verified = false;
|
||||
for(XmppAxolotlSession session : sessions) {
|
||||
if (session.getTrust().trusted()) {
|
||||
if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) {
|
||||
if (session.getTrust().isTrustedAndActive()) {
|
||||
if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) {
|
||||
verified = true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -817,55 +994,54 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return verified;
|
||||
}
|
||||
|
||||
public boolean hasPendingKeyFetches(Account account, Contact contact) {
|
||||
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
|
||||
AxolotlAddress foreignAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
|
||||
return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
|
||||
|| fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
|
||||
|
||||
public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
|
||||
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0);
|
||||
if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) {
|
||||
return true;
|
||||
}
|
||||
for(Jid jid : jids) {
|
||||
AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), 0);
|
||||
if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private XmppAxolotlMessage buildHeader(Contact contact) {
|
||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
|
||||
contact.getJid().toBareJid(), getOwnDeviceId());
|
||||
|
||||
Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
|
||||
Set<XmppAxolotlSession> ownSessions = findOwnSessions();
|
||||
if (contactSessions.isEmpty()) {
|
||||
return null;
|
||||
private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation conversation) {
|
||||
Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
|
||||
Collection<XmppAxolotlSession> ownSessions = findOwnSessions();
|
||||
if (remoteSessions.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
|
||||
for (XmppAxolotlSession session : contactSessions) {
|
||||
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
|
||||
for (XmppAxolotlSession session : remoteSessions) {
|
||||
axolotlMessage.addDevice(session);
|
||||
}
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
|
||||
for (XmppAxolotlSession session : ownSessions) {
|
||||
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
|
||||
axolotlMessage.addDevice(session);
|
||||
}
|
||||
|
||||
return axolotlMessage;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public XmppAxolotlMessage encrypt(Message message) {
|
||||
XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
|
||||
|
||||
if (axolotlMessage != null) {
|
||||
final String content;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
content = message.getFileParams().url.toString();
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
try {
|
||||
axolotlMessage.encrypt(content);
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
|
||||
final String content;
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
content = message.getFileParams().url.toString();
|
||||
} else {
|
||||
content = message.getBody();
|
||||
}
|
||||
try {
|
||||
axolotlMessage.encrypt(content);
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
if (!buildHeader(axolotlMessage,message.getConversation())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return axolotlMessage;
|
||||
|
@ -888,12 +1064,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
});
|
||||
}
|
||||
|
||||
public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
|
||||
public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
XmppAxolotlMessage axolotlMessage = buildHeader(contact);
|
||||
onMessageCreatedCallback.run(axolotlMessage);
|
||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
|
||||
if (buildHeader(axolotlMessage,conversation)) {
|
||||
onMessageCreatedCallback.run(axolotlMessage);
|
||||
} else {
|
||||
onMessageCreatedCallback.run(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -917,7 +1097,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
|
||||
private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
|
||||
AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
|
||||
AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toPreppedString(),
|
||||
message.getSenderDeviceId());
|
||||
XmppAxolotlSession session = sessions.get(senderAddress);
|
||||
if (session == null) {
|
||||
|
@ -942,7 +1122,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
session.resetPreKeyId();
|
||||
}
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from "+message.getFrom()+": " + e.getMessage());
|
||||
}
|
||||
|
||||
if (session.isFresh() && plaintextMessage != null) {
|
||||
|
@ -956,7 +1136,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
|
||||
|
||||
XmppAxolotlSession session = getReceivingSession(message);
|
||||
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
|
||||
try {
|
||||
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.d(Config.LOGTAG,"could not decrypt keyTransport message "+e.getMessage());
|
||||
keyTransportMessage = null;
|
||||
}
|
||||
|
||||
if (session.isFresh() && keyTransportMessage != null) {
|
||||
putFreshSession(session);
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
public class CryptoFailedException extends Exception {
|
||||
|
||||
public CryptoFailedException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
public CryptoFailedException(Exception e){
|
||||
super(e);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
||||
public class FingerprintStatus implements Comparable<FingerprintStatus> {
|
||||
|
||||
private static final long DO_NOT_OVERWRITE = -1;
|
||||
|
||||
private Trust trust = Trust.UNTRUSTED;
|
||||
private boolean active = false;
|
||||
private long lastActivation = DO_NOT_OVERWRITE;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
FingerprintStatus that = (FingerprintStatus) o;
|
||||
|
||||
return active == that.active && trust == that.trust;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = trust.hashCode();
|
||||
result = 31 * result + (active ? 1 : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
private FingerprintStatus() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
public ContentValues toContentValues() {
|
||||
final ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(SQLiteAxolotlStore.TRUST,trust.toString());
|
||||
contentValues.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0);
|
||||
if (lastActivation != DO_NOT_OVERWRITE) {
|
||||
contentValues.put(SQLiteAxolotlStore.LAST_ACTIVATION,lastActivation);
|
||||
}
|
||||
return contentValues;
|
||||
}
|
||||
|
||||
public static FingerprintStatus fromCursor(Cursor cursor) {
|
||||
final FingerprintStatus status = new FingerprintStatus();
|
||||
try {
|
||||
status.trust = Trust.valueOf(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.TRUST)));
|
||||
} catch(IllegalArgumentException e) {
|
||||
status.trust = Trust.UNTRUSTED;
|
||||
}
|
||||
status.active = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.ACTIVE)) > 0;
|
||||
status.lastActivation = cursor.getLong(cursor.getColumnIndex(SQLiteAxolotlStore.LAST_ACTIVATION));
|
||||
return status;
|
||||
}
|
||||
|
||||
public static FingerprintStatus createActiveUndecided() {
|
||||
final FingerprintStatus status = new FingerprintStatus();
|
||||
status.trust = Trust.UNDECIDED;
|
||||
status.active = true;
|
||||
status.lastActivation = System.currentTimeMillis();
|
||||
return status;
|
||||
}
|
||||
|
||||
public static FingerprintStatus createActiveTrusted() {
|
||||
final FingerprintStatus status = new FingerprintStatus();
|
||||
status.trust = Trust.TRUSTED;
|
||||
status.active = true;
|
||||
status.lastActivation = System.currentTimeMillis();
|
||||
return status;
|
||||
}
|
||||
|
||||
public static FingerprintStatus createActiveVerified(boolean x509) {
|
||||
final FingerprintStatus status = new FingerprintStatus();
|
||||
status.trust = x509 ? Trust.VERIFIED_X509 : Trust.VERIFIED;
|
||||
status.active = true;
|
||||
return status;
|
||||
}
|
||||
|
||||
public static FingerprintStatus createActive(boolean trusted) {
|
||||
final FingerprintStatus status = new FingerprintStatus();
|
||||
status.trust = trusted ? Trust.TRUSTED : Trust.UNTRUSTED;
|
||||
status.active = true;
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean isTrustedAndActive() {
|
||||
return active && isTrusted();
|
||||
}
|
||||
|
||||
public boolean isTrusted() {
|
||||
return trust == Trust.TRUSTED || isVerified();
|
||||
}
|
||||
|
||||
public boolean isVerified() {
|
||||
return trust == Trust.VERIFIED || trust == Trust.VERIFIED_X509;
|
||||
}
|
||||
|
||||
public boolean isCompromised() {
|
||||
return trust == Trust.COMPROMISED;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return active;
|
||||
}
|
||||
|
||||
public FingerprintStatus toActive() {
|
||||
FingerprintStatus status = new FingerprintStatus();
|
||||
status.trust = trust;
|
||||
if (!status.active) {
|
||||
status.lastActivation = System.currentTimeMillis();
|
||||
}
|
||||
status.active = true;
|
||||
return status;
|
||||
}
|
||||
|
||||
public FingerprintStatus toInactive() {
|
||||
FingerprintStatus status = new FingerprintStatus();
|
||||
status.trust = trust;
|
||||
status.active = false;
|
||||
return status;
|
||||
}
|
||||
|
||||
public Trust getTrust() {
|
||||
return trust;
|
||||
}
|
||||
|
||||
public FingerprintStatus toVerified() {
|
||||
FingerprintStatus status = new FingerprintStatus();
|
||||
status.active = active;
|
||||
status.trust = Trust.VERIFIED;
|
||||
return status;
|
||||
}
|
||||
|
||||
public FingerprintStatus toUntrusted() {
|
||||
FingerprintStatus status = new FingerprintStatus();
|
||||
status.active = active;
|
||||
status.trust = Trust.UNTRUSTED;
|
||||
return status;
|
||||
}
|
||||
|
||||
public static FingerprintStatus createInactiveVerified() {
|
||||
final FingerprintStatus status = new FingerprintStatus();
|
||||
status.trust = Trust.VERIFIED;
|
||||
status.active = false;
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(FingerprintStatus o) {
|
||||
if (active == o.active) {
|
||||
if (lastActivation > o.lastActivation) {
|
||||
return -1;
|
||||
} else if (lastActivation < o.lastActivation) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else if (active){
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
public long getLastActivation() {
|
||||
return lastActivation;
|
||||
}
|
||||
|
||||
public enum Trust {
|
||||
COMPROMISED,
|
||||
UNDECIDED,
|
||||
UNTRUSTED,
|
||||
TRUSTED,
|
||||
VERIFIED,
|
||||
VERIFIED_X509
|
||||
}
|
||||
|
||||
}
|
|
@ -21,7 +21,10 @@ import java.util.Set;
|
|||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
||||
public class SQLiteAxolotlStore implements AxolotlStore {
|
||||
|
||||
|
@ -35,7 +38,10 @@ public class SQLiteAxolotlStore implements AxolotlStore {
|
|||
public static final String KEY = "key";
|
||||
public static final String FINGERPRINT = "fingerprint";
|
||||
public static final String NAME = "name";
|
||||
public static final String TRUSTED = "trusted";
|
||||
public static final String TRUSTED = "trusted"; //no longer used
|
||||
public static final String TRUST = "trust";
|
||||
public static final String ACTIVE = "active";
|
||||
public static final String LAST_ACTIVATION = "last_activation";
|
||||
public static final String OWN = "ownkey";
|
||||
public static final String CERTIFICATE = "certificate";
|
||||
|
||||
|
@ -51,11 +57,11 @@ public class SQLiteAxolotlStore implements AxolotlStore {
|
|||
private int localRegistrationId;
|
||||
private int currentPreKeyId = 0;
|
||||
|
||||
private final LruCache<String, XmppAxolotlSession.Trust> trustCache =
|
||||
new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) {
|
||||
private final LruCache<String, FingerprintStatus> trustCache =
|
||||
new LruCache<String, FingerprintStatus>(NUM_TRUSTS_TO_CACHE) {
|
||||
@Override
|
||||
protected XmppAxolotlSession.Trust create(String fingerprint) {
|
||||
return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
|
||||
protected FingerprintStatus create(String fingerprint) {
|
||||
return mXmppConnectionService.databaseBackend.getFingerprintStatus(account, fingerprint);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -90,16 +96,18 @@ public class SQLiteAxolotlStore implements AxolotlStore {
|
|||
// --------------------------------------
|
||||
|
||||
private IdentityKeyPair loadIdentityKeyPair() {
|
||||
IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account);
|
||||
synchronized (mXmppConnectionService) {
|
||||
IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account);
|
||||
|
||||
if (ownKey != null) {
|
||||
if (ownKey != null) {
|
||||
return ownKey;
|
||||
} else {
|
||||
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve own IdentityKeyPair");
|
||||
ownKey = generateIdentityKeyPair();
|
||||
mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey);
|
||||
}
|
||||
return ownKey;
|
||||
} else {
|
||||
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve own IdentityKeyPair");
|
||||
ownKey = generateIdentityKeyPair();
|
||||
mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey);
|
||||
}
|
||||
return ownKey;
|
||||
}
|
||||
|
||||
private int loadRegistrationId() {
|
||||
|
@ -125,15 +133,15 @@ public class SQLiteAxolotlStore implements AxolotlStore {
|
|||
}
|
||||
|
||||
private int loadCurrentPreKeyId() {
|
||||
String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
|
||||
int reg_id;
|
||||
if (regIdString != null) {
|
||||
reg_id = Integer.valueOf(regIdString);
|
||||
String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
|
||||
int prekey_id;
|
||||
if (prekeyIdString != null) {
|
||||
prekey_id = Integer.valueOf(prekeyIdString);
|
||||
} else {
|
||||
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid());
|
||||
reg_id = 0;
|
||||
prekey_id = 0;
|
||||
}
|
||||
return reg_id;
|
||||
return prekey_id;
|
||||
}
|
||||
|
||||
public void regenerate() {
|
||||
|
@ -183,7 +191,20 @@ public class SQLiteAxolotlStore implements AxolotlStore {
|
|||
@Override
|
||||
public void saveIdentity(String name, IdentityKey identityKey) {
|
||||
if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
|
||||
mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
|
||||
String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
|
||||
FingerprintStatus status = getFingerprintStatus(fingerprint);
|
||||
if (status == null) {
|
||||
if (mXmppConnectionService.blindTrustBeforeVerification() && !account.getAxolotlService().hasVerifiedKeys(name)) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": blindly trusted "+fingerprint+" of "+name);
|
||||
status = FingerprintStatus.createActiveTrusted();
|
||||
} else {
|
||||
status = FingerprintStatus.createActiveUndecided();
|
||||
}
|
||||
} else {
|
||||
status = status.toActive();
|
||||
}
|
||||
mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey, status);
|
||||
trustCache.remove(fingerprint);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -206,12 +227,12 @@ public class SQLiteAxolotlStore implements AxolotlStore {
|
|||
return true;
|
||||
}
|
||||
|
||||
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
|
||||
public FingerprintStatus getFingerprintStatus(String fingerprint) {
|
||||
return (fingerprint == null)? null : trustCache.get(fingerprint);
|
||||
}
|
||||
|
||||
public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
|
||||
mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
|
||||
public void setFingerprintStatus(String fingerprint, FingerprintStatus status) {
|
||||
mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status);
|
||||
trustCache.remove(fingerprint);
|
||||
}
|
||||
|
||||
|
@ -223,8 +244,8 @@ public class SQLiteAxolotlStore implements AxolotlStore {
|
|||
return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint);
|
||||
}
|
||||
|
||||
public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) {
|
||||
return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
|
||||
public Set<IdentityKey> getContactKeysWithTrust(String bareJid, FingerprintStatus status) {
|
||||
return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status);
|
||||
}
|
||||
|
||||
public long getContactNumTrustedKeys(String bareJid) {
|
||||
|
@ -426,4 +447,8 @@ public class SQLiteAxolotlStore implements AxolotlStore {
|
|||
public void removeSignedPreKey(int signedPreKeyId) {
|
||||
mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
|
||||
}
|
||||
|
||||
public void preVerifyFingerprint(Account account, String name, String fingerprint) {
|
||||
mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.siacs.conversations.crypto.axolotl;
|
|||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
@ -40,8 +41,9 @@ public class XmppAxolotlMessage {
|
|||
|
||||
private byte[] innerKey;
|
||||
private byte[] ciphertext = null;
|
||||
private byte[] authtagPlusInnerKey = null;
|
||||
private byte[] iv = null;
|
||||
private final Map<Integer, byte[]> keys;
|
||||
private final Map<Integer, XmppAxolotlSession.AxolotlKey> keys;
|
||||
private final Jid from;
|
||||
private final int sourceDeviceId;
|
||||
|
||||
|
@ -91,7 +93,11 @@ public class XmppAxolotlMessage {
|
|||
private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
|
||||
this.from = from;
|
||||
Element header = axolotlMessage.findChild(HEADER);
|
||||
this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
|
||||
try {
|
||||
this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("invalid source id");
|
||||
}
|
||||
List<Element> keyElements = header.getChildren();
|
||||
this.keys = new HashMap<>(keyElements.size());
|
||||
for (Element keyElement : keyElements) {
|
||||
|
@ -99,17 +105,18 @@ public class XmppAxolotlMessage {
|
|||
case KEYTAG:
|
||||
try {
|
||||
Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
|
||||
byte[] key = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
|
||||
this.keys.put(recipientId, key);
|
||||
byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
|
||||
boolean isPreKey =keyElement.getAttributeAsBoolean("prekey");
|
||||
this.keys.put(recipientId, new XmppAxolotlSession.AxolotlKey(key,isPreKey));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
throw new IllegalArgumentException("invalid remote id");
|
||||
}
|
||||
break;
|
||||
case IVTAG:
|
||||
if (this.iv != null) {
|
||||
throw new IllegalArgumentException("Duplicate iv entry");
|
||||
}
|
||||
iv = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
|
||||
iv = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
|
||||
break;
|
||||
default:
|
||||
Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString());
|
||||
|
@ -118,7 +125,7 @@ public class XmppAxolotlMessage {
|
|||
}
|
||||
Element payloadElement = axolotlMessage.findChild(PAYLOAD);
|
||||
if (payloadElement != null) {
|
||||
ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT);
|
||||
ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,8 +165,15 @@ public class XmppAxolotlMessage {
|
|||
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
|
||||
this.innerKey = secretKey.getEncoded();
|
||||
this.ciphertext = cipher.doFinal(plaintext.getBytes());
|
||||
this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes());
|
||||
if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) {
|
||||
this.authtagPlusInnerKey = new byte[16+16];
|
||||
byte[] ciphertext = new byte[this.ciphertext.length - 16];
|
||||
System.arraycopy(this.ciphertext,0,ciphertext,0,ciphertext.length);
|
||||
System.arraycopy(this.ciphertext,ciphertext.length,authtagPlusInnerKey,16,16);
|
||||
System.arraycopy(this.innerKey,0,authtagPlusInnerKey,0,this.innerKey.length);
|
||||
this.ciphertext = ciphertext;
|
||||
}
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||||
| IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
|
||||
| InvalidAlgorithmParameterException e) {
|
||||
|
@ -167,6 +181,22 @@ public class XmppAxolotlMessage {
|
|||
}
|
||||
}
|
||||
|
||||
private static byte[] getPaddedBytes(String plaintext) {
|
||||
int plainLength = plaintext.getBytes().length;
|
||||
int pad = Math.max(64,(plainLength / 32 + 1) * 32) - plainLength;
|
||||
SecureRandom random = new SecureRandom();
|
||||
int left = random.nextInt(pad);
|
||||
int right = pad - left;
|
||||
StringBuilder builder = new StringBuilder(plaintext);
|
||||
for(int i = 0; i < left; ++i) {
|
||||
builder.insert(0,random.nextBoolean() ? "\t" : " ");
|
||||
}
|
||||
for(int i = 0; i < right; ++i) {
|
||||
builder.append(random.nextBoolean() ? "\t" : " ");
|
||||
}
|
||||
return builder.toString().getBytes();
|
||||
}
|
||||
|
||||
public Jid getFrom() {
|
||||
return this.from;
|
||||
}
|
||||
|
@ -180,7 +210,12 @@ public class XmppAxolotlMessage {
|
|||
}
|
||||
|
||||
public void addDevice(XmppAxolotlSession session) {
|
||||
byte[] key = session.processSending(innerKey);
|
||||
XmppAxolotlSession.AxolotlKey key;
|
||||
if (authtagPlusInnerKey != null) {
|
||||
key = session.processSending(authtagPlusInnerKey);
|
||||
} else {
|
||||
key = session.processSending(innerKey);
|
||||
}
|
||||
if (key != null) {
|
||||
keys.put(session.getRemoteAddress().getDeviceId(), key);
|
||||
}
|
||||
|
@ -198,30 +233,33 @@ public class XmppAxolotlMessage {
|
|||
Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX);
|
||||
Element headerElement = encryptionElement.addChild(HEADER);
|
||||
headerElement.setAttribute(SOURCEID, sourceDeviceId);
|
||||
for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) {
|
||||
for (Map.Entry<Integer, XmppAxolotlSession.AxolotlKey> keyEntry : keys.entrySet()) {
|
||||
Element keyElement = new Element(KEYTAG);
|
||||
keyElement.setAttribute(REMOTEID, keyEntry.getKey());
|
||||
keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT));
|
||||
if (keyEntry.getValue().prekey) {
|
||||
keyElement.setAttribute("prekey","true");
|
||||
}
|
||||
keyElement.setContent(Base64.encodeToString(keyEntry.getValue().key, Base64.NO_WRAP));
|
||||
headerElement.addChild(keyElement);
|
||||
}
|
||||
headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
|
||||
headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP));
|
||||
if (ciphertext != null) {
|
||||
Element payload = encryptionElement.addChild(PAYLOAD);
|
||||
payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
|
||||
payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP));
|
||||
}
|
||||
return encryptionElement;
|
||||
}
|
||||
|
||||
private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) {
|
||||
byte[] encryptedKey = keys.get(sourceDeviceId);
|
||||
return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null;
|
||||
private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
|
||||
XmppAxolotlSession.AxolotlKey encryptedKey = keys.get(sourceDeviceId);
|
||||
if (encryptedKey == null) {
|
||||
throw new CryptoFailedException("Message was not encrypted for this device");
|
||||
}
|
||||
return session.processReceiving(encryptedKey);
|
||||
}
|
||||
|
||||
public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) {
|
||||
byte[] key = unpackKey(session, sourceDeviceId);
|
||||
return (key != null)
|
||||
? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV())
|
||||
: null;
|
||||
public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
|
||||
return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV());
|
||||
}
|
||||
|
||||
public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
|
||||
|
@ -229,6 +267,19 @@ public class XmppAxolotlMessage {
|
|||
byte[] key = unpackKey(session, sourceDeviceId);
|
||||
if (key != null) {
|
||||
try {
|
||||
|
||||
if (key.length >= 32) {
|
||||
int authtaglength = key.length - 16;
|
||||
Log.d(Config.LOGTAG,"found auth tag as part of omemo key");
|
||||
byte[] newCipherText = new byte[key.length - 16 + ciphertext.length];
|
||||
byte[] newKey = new byte[16];
|
||||
System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length);
|
||||
System.arraycopy(key, 16, newCipherText, ciphertext.length, authtaglength);
|
||||
System.arraycopy(key,0,newKey,0,newKey.length);
|
||||
ciphertext = newCipherText;
|
||||
key = newKey;
|
||||
}
|
||||
|
||||
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||
|
@ -236,7 +287,7 @@ public class XmppAxolotlMessage {
|
|||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||
|
||||
String plaintext = new String(cipher.doFinal(ciphertext));
|
||||
plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint());
|
||||
plaintextMessage = new XmppAxolotlPlaintextMessage(Config.OMEMO_PADDING ? plaintext.trim() : plaintext, session.getFingerprint());
|
||||
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
|
||||
| InvalidAlgorithmParameterException | IllegalBlockSizeException
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.support.annotation.NonNull;
|
|||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import org.bouncycastle.math.ec.PreCompInfo;
|
||||
import org.whispersystems.libaxolotl.AxolotlAddress;
|
||||
import org.whispersystems.libaxolotl.DuplicateMessageException;
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
|
@ -18,14 +19,13 @@ import org.whispersystems.libaxolotl.UntrustedIdentityException;
|
|||
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
||||
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
||||
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
|
||||
public class XmppAxolotlSession {
|
||||
public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> {
|
||||
private final SessionCipher cipher;
|
||||
private final SQLiteAxolotlStore sqLiteAxolotlStore;
|
||||
private final AxolotlAddress remoteAddress;
|
||||
|
@ -34,76 +34,6 @@ public class XmppAxolotlSession {
|
|||
private Integer preKeyId = null;
|
||||
private boolean fresh = true;
|
||||
|
||||
public enum Trust {
|
||||
UNDECIDED(0),
|
||||
TRUSTED(1),
|
||||
UNTRUSTED(2),
|
||||
COMPROMISED(3),
|
||||
INACTIVE_TRUSTED(4),
|
||||
INACTIVE_UNDECIDED(5),
|
||||
INACTIVE_UNTRUSTED(6),
|
||||
TRUSTED_X509(7),
|
||||
INACTIVE_TRUSTED_X509(8);
|
||||
|
||||
private static final Map<Integer, Trust> trustsByValue = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (Trust trust : Trust.values()) {
|
||||
trustsByValue.put(trust.getCode(), trust);
|
||||
}
|
||||
}
|
||||
|
||||
private final int code;
|
||||
|
||||
Trust(int code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public int getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
switch (this) {
|
||||
case UNDECIDED:
|
||||
return "Trust undecided " + getCode();
|
||||
case TRUSTED:
|
||||
return "Trusted " + getCode();
|
||||
case COMPROMISED:
|
||||
return "Compromised " + getCode();
|
||||
case INACTIVE_TRUSTED:
|
||||
return "Inactive (Trusted)" + getCode();
|
||||
case INACTIVE_UNDECIDED:
|
||||
return "Inactive (Undecided)" + getCode();
|
||||
case INACTIVE_UNTRUSTED:
|
||||
return "Inactive (Untrusted)" + getCode();
|
||||
case TRUSTED_X509:
|
||||
return "Trusted (X509) " + getCode();
|
||||
case INACTIVE_TRUSTED_X509:
|
||||
return "Inactive (Trusted (X509)) " + getCode();
|
||||
case UNTRUSTED:
|
||||
default:
|
||||
return "Untrusted " + getCode();
|
||||
}
|
||||
}
|
||||
|
||||
public static Trust fromBoolean(Boolean trusted) {
|
||||
return trusted ? TRUSTED : UNTRUSTED;
|
||||
}
|
||||
|
||||
public static Trust fromCode(int code) {
|
||||
return trustsByValue.get(code);
|
||||
}
|
||||
|
||||
public boolean trusted() {
|
||||
return this == TRUSTED_X509 || this == TRUSTED;
|
||||
}
|
||||
|
||||
public boolean trustedInactive() {
|
||||
return this == INACTIVE_TRUSTED_X509 || this == INACTIVE_TRUSTED;
|
||||
}
|
||||
}
|
||||
|
||||
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, IdentityKey identityKey) {
|
||||
this(account, store, remoteAddress);
|
||||
this.identityKey = identityKey;
|
||||
|
@ -145,77 +75,86 @@ public class XmppAxolotlSession {
|
|||
this.fresh = false;
|
||||
}
|
||||
|
||||
protected void setTrust(Trust trust) {
|
||||
sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), trust);
|
||||
protected void setTrust(FingerprintStatus status) {
|
||||
sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status);
|
||||
}
|
||||
|
||||
protected Trust getTrust() {
|
||||
Trust trust = sqLiteAxolotlStore.getFingerprintTrust(getFingerprint());
|
||||
return (trust == null) ? Trust.UNDECIDED : trust;
|
||||
public FingerprintStatus getTrust() {
|
||||
FingerprintStatus status = sqLiteAxolotlStore.getFingerprintStatus(getFingerprint());
|
||||
return (status == null) ? FingerprintStatus.createActiveUndecided() : status;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public byte[] processReceiving(byte[] encryptedKey) {
|
||||
byte[] plaintext = null;
|
||||
Trust trust = getTrust();
|
||||
switch (trust) {
|
||||
case INACTIVE_TRUSTED:
|
||||
case UNDECIDED:
|
||||
case UNTRUSTED:
|
||||
case TRUSTED:
|
||||
case INACTIVE_TRUSTED_X509:
|
||||
case TRUSTED_X509:
|
||||
public byte[] processReceiving(AxolotlKey encryptedKey) throws CryptoFailedException {
|
||||
byte[] plaintext;
|
||||
FingerprintStatus status = getTrust();
|
||||
if (!status.isCompromised()) {
|
||||
try {
|
||||
CiphertextMessage ciphertextMessage;
|
||||
try {
|
||||
try {
|
||||
PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
|
||||
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
|
||||
IdentityKey msgIdentityKey = message.getIdentityKey();
|
||||
if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) {
|
||||
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint());
|
||||
} else {
|
||||
this.identityKey = msgIdentityKey;
|
||||
plaintext = cipher.decrypt(message);
|
||||
if (message.getPreKeyId().isPresent()) {
|
||||
preKeyId = message.getPreKeyId().get();
|
||||
}
|
||||
}
|
||||
} catch (InvalidMessageException | InvalidVersionException e) {
|
||||
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received");
|
||||
WhisperMessage message = new WhisperMessage(encryptedKey);
|
||||
plaintext = cipher.decrypt(message);
|
||||
} catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
|
||||
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
|
||||
ciphertextMessage = new PreKeyWhisperMessage(encryptedKey.key);
|
||||
Optional<Integer> optionalPreKeyId = ((PreKeyWhisperMessage) ciphertextMessage).getPreKeyId();
|
||||
IdentityKey identityKey = ((PreKeyWhisperMessage) ciphertextMessage).getIdentityKey();
|
||||
if (!optionalPreKeyId.isPresent()) {
|
||||
throw new CryptoFailedException("PreKeyWhisperMessage did not contain a PreKeyId");
|
||||
}
|
||||
} catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
|
||||
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
|
||||
}
|
||||
|
||||
if (plaintext != null) {
|
||||
if (trust == Trust.INACTIVE_TRUSTED) {
|
||||
setTrust(Trust.TRUSTED);
|
||||
} else if (trust == Trust.INACTIVE_TRUSTED_X509) {
|
||||
setTrust(Trust.TRUSTED_X509);
|
||||
preKeyId = optionalPreKeyId.get();
|
||||
if (this.identityKey != null && !this.identityKey.equals(identityKey)) {
|
||||
throw new CryptoFailedException("Received PreKeyWhisperMessage but preexisting identity key changed.");
|
||||
}
|
||||
this.identityKey = identityKey;
|
||||
} catch (InvalidVersionException | InvalidMessageException e) {
|
||||
ciphertextMessage = new WhisperMessage(encryptedKey.key);
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case COMPROMISED:
|
||||
default:
|
||||
// ignore
|
||||
break;
|
||||
if (ciphertextMessage instanceof PreKeyWhisperMessage) {
|
||||
plaintext = cipher.decrypt((PreKeyWhisperMessage) ciphertextMessage);
|
||||
} else {
|
||||
plaintext = cipher.decrypt((WhisperMessage) ciphertextMessage);
|
||||
}
|
||||
} catch (InvalidKeyException | LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException | InvalidKeyIdException | UntrustedIdentityException e) {
|
||||
if (!(e instanceof DuplicateMessageException)) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
throw new CryptoFailedException("Error decrypting WhisperMessage " + e.getClass().getSimpleName() + ": " + e.getMessage());
|
||||
}
|
||||
if (!status.isActive()) {
|
||||
setTrust(status.toActive());
|
||||
}
|
||||
} else {
|
||||
throw new CryptoFailedException("not encrypting omemo message from fingerprint "+getFingerprint()+" because it was marked as compromised");
|
||||
}
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public byte[] processSending(@NonNull byte[] outgoingMessage) {
|
||||
Trust trust = getTrust();
|
||||
if (trust.trusted()) {
|
||||
public AxolotlKey processSending(@NonNull byte[] outgoingMessage) {
|
||||
FingerprintStatus status = getTrust();
|
||||
if (status.isTrustedAndActive()) {
|
||||
CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
|
||||
return ciphertextMessage.serialize();
|
||||
return new AxolotlKey(ciphertextMessage.serialize(),ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(XmppAxolotlSession o) {
|
||||
return getTrust().compareTo(o.getTrust());
|
||||
}
|
||||
|
||||
public static class AxolotlKey {
|
||||
|
||||
|
||||
public final byte[] key;
|
||||
public final boolean prekey;
|
||||
|
||||
public AxolotlKey(byte[] key, boolean prekey) {
|
||||
this.key = key;
|
||||
this.prekey = prekey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
public class Anonymous extends SaslMechanism {
|
||||
|
||||
public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return "ANONYMOUS";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,228 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.os.Build;
|
||||
import android.util.Base64;
|
||||
import android.util.LruCache;
|
||||
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
|
||||
abstract class ScramMechanism extends SaslMechanism {
|
||||
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
|
||||
private final static String GS2_HEADER = "n,,";
|
||||
private String clientFirstMessageBare;
|
||||
private final String clientNonce;
|
||||
private byte[] serverSignature = null;
|
||||
static HMac HMAC;
|
||||
static Digest DIGEST;
|
||||
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
|
||||
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
|
||||
|
||||
private static class KeyPair {
|
||||
final byte[] clientKey;
|
||||
final byte[] serverKey;
|
||||
|
||||
KeyPair(final byte[] clientKey, final byte[] serverKey) {
|
||||
this.clientKey = clientKey;
|
||||
this.serverKey = serverKey;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
CACHE = new LruCache<String, KeyPair>(10) {
|
||||
protected KeyPair create(final String k) {
|
||||
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
|
||||
// Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()'
|
||||
// is applied to prevent commas in the strings breaking things.
|
||||
final String[] kparts = k.split(",", 4);
|
||||
try {
|
||||
final byte[] saltedPassword, serverKey, clientKey;
|
||||
saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(),
|
||||
Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3]));
|
||||
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
|
||||
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
|
||||
|
||||
return new KeyPair(clientKey, serverKey);
|
||||
} catch (final InvalidKeyException | NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static final LruCache<String, KeyPair> CACHE;
|
||||
|
||||
protected State state = State.INITIAL;
|
||||
|
||||
ScramMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
|
||||
// This nonce should be different for each authentication attempt.
|
||||
clientNonce = new BigInteger(100, this.rng).toString(32);
|
||||
clientFirstMessageBare = "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
|
||||
clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) +
|
||||
",r=" + this.clientNonce;
|
||||
state = State.AUTH_TEXT_SENT;
|
||||
}
|
||||
return Base64.encodeToString(
|
||||
(GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
|
||||
Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResponse(final String challenge) throws AuthenticationException {
|
||||
switch (state) {
|
||||
case AUTH_TEXT_SENT:
|
||||
if (challenge == null) {
|
||||
throw new AuthenticationException("challenge can not be null");
|
||||
}
|
||||
byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
|
||||
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
|
||||
String nonce = "";
|
||||
int iterationCount = -1;
|
||||
String salt = "";
|
||||
for (final String token : tokenizer) {
|
||||
if (token.charAt(1) == '=') {
|
||||
switch (token.charAt(0)) {
|
||||
case 'i':
|
||||
try {
|
||||
iterationCount = Integer.parseInt(token.substring(2));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
salt = token.substring(2);
|
||||
break;
|
||||
case 'r':
|
||||
nonce = token.substring(2);
|
||||
break;
|
||||
case 'm':
|
||||
/*
|
||||
* RFC 5802:
|
||||
* m: This attribute is reserved for future extensibility. In this
|
||||
* version of SCRAM, its presence in a client or a server message
|
||||
* MUST cause authentication failure when the attribute is parsed by
|
||||
* the other end.
|
||||
*/
|
||||
throw new AuthenticationException("Server sent reserved token: `m'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (iterationCount < 0) {
|
||||
throw new AuthenticationException("Server did not send iteration count");
|
||||
}
|
||||
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
|
||||
throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce);
|
||||
}
|
||||
if (salt.isEmpty()) {
|
||||
throw new AuthenticationException("Server sent empty salt");
|
||||
}
|
||||
|
||||
final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString(
|
||||
GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce;
|
||||
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
|
||||
+ clientFinalMessageWithoutProof).getBytes();
|
||||
|
||||
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
|
||||
final KeyPair keys = CACHE.get(
|
||||
CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + ","
|
||||
+ CryptoHelper.bytesToHex(account.getPassword().getBytes()) + ","
|
||||
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
|
||||
+ String.valueOf(iterationCount)
|
||||
);
|
||||
if (keys == null) {
|
||||
throw new AuthenticationException("Invalid keys generated");
|
||||
}
|
||||
final byte[] clientSignature;
|
||||
try {
|
||||
serverSignature = hmac(keys.serverKey, authMessage);
|
||||
final byte[] storedKey = digest(keys.clientKey);
|
||||
|
||||
clientSignature = hmac(storedKey, authMessage);
|
||||
|
||||
} catch (final InvalidKeyException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
|
||||
final byte[] clientProof = new byte[keys.clientKey.length];
|
||||
|
||||
for (int i = 0; i < clientProof.length; i++) {
|
||||
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
|
||||
}
|
||||
|
||||
|
||||
final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" +
|
||||
Base64.encodeToString(clientProof, Base64.NO_WRAP);
|
||||
state = State.RESPONSE_SENT;
|
||||
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
|
||||
case RESPONSE_SENT:
|
||||
try {
|
||||
final String clientCalculatedServerFinalMessage = "v=" +
|
||||
Base64.encodeToString(serverSignature, Base64.NO_WRAP);
|
||||
if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) {
|
||||
throw new Exception();
|
||||
}
|
||||
state = State.VALID_SERVER_RESPONSE;
|
||||
return "";
|
||||
} catch(Exception e) {
|
||||
throw new AuthenticationException("Server final message does not match calculated final message");
|
||||
}
|
||||
default:
|
||||
throw new InvalidStateException(state);
|
||||
}
|
||||
}
|
||||
|
||||
private static synchronized byte[] hmac(final byte[] key, final byte[] input)
|
||||
throws InvalidKeyException {
|
||||
HMAC.init(new KeyParameter(key));
|
||||
HMAC.update(input, 0, input.length);
|
||||
final byte[] out = new byte[HMAC.getMacSize()];
|
||||
HMAC.doFinal(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static synchronized byte[] digest(byte[] bytes) {
|
||||
DIGEST.reset();
|
||||
DIGEST.update(bytes, 0, bytes.length);
|
||||
final byte[] out = new byte[DIGEST.getDigestSize()];
|
||||
DIGEST.doFinal(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the
|
||||
* pseudorandom function (PRF) and with dkLen == output length of
|
||||
* HMAC() == output length of H().
|
||||
*/
|
||||
private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations)
|
||||
throws InvalidKeyException {
|
||||
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
|
||||
byte[] out = u.clone();
|
||||
for (int i = 1; i < iterations; i++) {
|
||||
u = hmac(key, u);
|
||||
for (int j = 0; j < u.length; j++) {
|
||||
out[j] ^= u[j];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
|
@ -1,77 +1,21 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import android.util.Base64;
|
||||
import android.util.LruCache;
|
||||
|
||||
import org.bouncycastle.crypto.Digest;
|
||||
import org.bouncycastle.crypto.digests.SHA1Digest;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.Charset;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
public class ScramSha1 extends SaslMechanism {
|
||||
// TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage.
|
||||
final private static String GS2_HEADER = "n,,";
|
||||
private String clientFirstMessageBare;
|
||||
final private String clientNonce;
|
||||
private byte[] serverSignature = null;
|
||||
private static HMac HMAC;
|
||||
private static Digest DIGEST;
|
||||
private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes();
|
||||
private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes();
|
||||
|
||||
public static class KeyPair {
|
||||
final public byte[] clientKey;
|
||||
final public byte[] serverKey;
|
||||
|
||||
public KeyPair(final byte[] clientKey, final byte[] serverKey) {
|
||||
this.clientKey = clientKey;
|
||||
this.serverKey = serverKey;
|
||||
}
|
||||
}
|
||||
|
||||
private static final LruCache<String, KeyPair> CACHE;
|
||||
|
||||
public class ScramSha1 extends ScramMechanism {
|
||||
static {
|
||||
DIGEST = new SHA1Digest();
|
||||
HMAC = new HMac(new SHA1Digest());
|
||||
CACHE = new LruCache<String, KeyPair>(10) {
|
||||
protected KeyPair create(final String k) {
|
||||
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
|
||||
// Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()'
|
||||
// is applied to prevent commas in the strings breaking things.
|
||||
final String[] kparts = k.split(",", 4);
|
||||
try {
|
||||
final byte[] saltedPassword, serverKey, clientKey;
|
||||
saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(),
|
||||
Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3]));
|
||||
serverKey = hmac(saltedPassword, SERVER_KEY_BYTES);
|
||||
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
|
||||
|
||||
return new KeyPair(clientKey, serverKey);
|
||||
} catch (final InvalidKeyException | NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private State state = State.INITIAL;
|
||||
|
||||
public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
|
||||
// This nonce should be different for each authentication attempt.
|
||||
clientNonce = new BigInteger(100, this.rng).toString(32);
|
||||
clientFirstMessageBare = "";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -83,152 +27,4 @@ public class ScramSha1 extends SaslMechanism {
|
|||
public String getMechanism() {
|
||||
return "SCRAM-SHA-1";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getClientFirstMessage() {
|
||||
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
|
||||
clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) +
|
||||
",r=" + this.clientNonce;
|
||||
state = State.AUTH_TEXT_SENT;
|
||||
}
|
||||
return Base64.encodeToString(
|
||||
(GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
|
||||
Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getResponse(final String challenge) throws AuthenticationException {
|
||||
switch (state) {
|
||||
case AUTH_TEXT_SENT:
|
||||
if (challenge == null) {
|
||||
throw new AuthenticationException("challenge can not be null");
|
||||
}
|
||||
byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
|
||||
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
|
||||
String nonce = "";
|
||||
int iterationCount = -1;
|
||||
String salt = "";
|
||||
for (final String token : tokenizer) {
|
||||
if (token.charAt(1) == '=') {
|
||||
switch (token.charAt(0)) {
|
||||
case 'i':
|
||||
try {
|
||||
iterationCount = Integer.parseInt(token.substring(2));
|
||||
} catch (final NumberFormatException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
break;
|
||||
case 's':
|
||||
salt = token.substring(2);
|
||||
break;
|
||||
case 'r':
|
||||
nonce = token.substring(2);
|
||||
break;
|
||||
case 'm':
|
||||
/*
|
||||
* RFC 5802:
|
||||
* m: This attribute is reserved for future extensibility. In this
|
||||
* version of SCRAM, its presence in a client or a server message
|
||||
* MUST cause authentication failure when the attribute is parsed by
|
||||
* the other end.
|
||||
*/
|
||||
throw new AuthenticationException("Server sent reserved token: `m'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (iterationCount < 0) {
|
||||
throw new AuthenticationException("Server did not send iteration count");
|
||||
}
|
||||
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
|
||||
throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce);
|
||||
}
|
||||
if (salt.isEmpty()) {
|
||||
throw new AuthenticationException("Server sent empty salt");
|
||||
}
|
||||
|
||||
final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString(
|
||||
GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce;
|
||||
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
|
||||
+ clientFinalMessageWithoutProof).getBytes();
|
||||
|
||||
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
|
||||
final KeyPair keys = CACHE.get(
|
||||
CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + ","
|
||||
+ CryptoHelper.bytesToHex(account.getPassword().getBytes()) + ","
|
||||
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
|
||||
+ String.valueOf(iterationCount)
|
||||
);
|
||||
if (keys == null) {
|
||||
throw new AuthenticationException("Invalid keys generated");
|
||||
}
|
||||
final byte[] clientSignature;
|
||||
try {
|
||||
serverSignature = hmac(keys.serverKey, authMessage);
|
||||
final byte[] storedKey = digest(keys.clientKey);
|
||||
|
||||
clientSignature = hmac(storedKey, authMessage);
|
||||
|
||||
} catch (final InvalidKeyException e) {
|
||||
throw new AuthenticationException(e);
|
||||
}
|
||||
|
||||
final byte[] clientProof = new byte[keys.clientKey.length];
|
||||
|
||||
for (int i = 0; i < clientProof.length; i++) {
|
||||
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
|
||||
}
|
||||
|
||||
|
||||
final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" +
|
||||
Base64.encodeToString(clientProof, Base64.NO_WRAP);
|
||||
state = State.RESPONSE_SENT;
|
||||
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
|
||||
case RESPONSE_SENT:
|
||||
final String clientCalculatedServerFinalMessage = "v=" +
|
||||
Base64.encodeToString(serverSignature, Base64.NO_WRAP);
|
||||
if (challenge == null || !clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) {
|
||||
throw new AuthenticationException("Server final message does not match calculated final message");
|
||||
}
|
||||
state = State.VALID_SERVER_RESPONSE;
|
||||
return "";
|
||||
default:
|
||||
throw new InvalidStateException(state);
|
||||
}
|
||||
}
|
||||
|
||||
public static synchronized byte[] hmac(final byte[] key, final byte[] input)
|
||||
throws InvalidKeyException {
|
||||
HMAC.init(new KeyParameter(key));
|
||||
HMAC.update(input, 0, input.length);
|
||||
final byte[] out = new byte[HMAC.getMacSize()];
|
||||
HMAC.doFinal(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
public static synchronized byte[] digest(byte[] bytes) {
|
||||
DIGEST.reset();
|
||||
DIGEST.update(bytes, 0, bytes.length);
|
||||
final byte[] out = new byte[DIGEST.getDigestSize()];
|
||||
DIGEST.doFinal(out, 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the
|
||||
* pseudorandom function (PRF) and with dkLen == output length of
|
||||
* HMAC() == output length of H().
|
||||
*/
|
||||
private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations)
|
||||
throws InvalidKeyException {
|
||||
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
|
||||
byte[] out = u.clone();
|
||||
for (int i = 1; i < iterations; i++) {
|
||||
u = hmac(key, u);
|
||||
for (int j = 0; j < u.length; j++) {
|
||||
out[j] ^= u[j];
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
package eu.siacs.conversations.crypto.sasl;
|
||||
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest;
|
||||
import org.bouncycastle.crypto.macs.HMac;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.xml.TagWriter;
|
||||
|
||||
public class ScramSha256 extends ScramMechanism {
|
||||
static {
|
||||
DIGEST = new SHA256Digest();
|
||||
HMAC = new HMac(new SHA256Digest());
|
||||
}
|
||||
|
||||
public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) {
|
||||
super(tagWriter, account, rng);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPriority() {
|
||||
return 25;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMechanism() {
|
||||
return "SCRAM-SHA-256";
|
||||
}
|
||||
}
|
|
@ -3,8 +3,10 @@ package eu.siacs.conversations.entities;
|
|||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Pair;
|
||||
|
||||
import eu.siacs.conversations.crypto.PgpDecryptionService;
|
||||
|
||||
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
|
||||
import net.java.otr4j.crypto.OtrCryptoException;
|
||||
|
||||
|
@ -13,16 +15,20 @@ import org.json.JSONObject;
|
|||
|
||||
import java.security.PublicKey;
|
||||
import java.security.interfaces.DSAPublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.OtrService;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.XmppUri;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
@ -41,6 +47,8 @@ public class Account extends AbstractEntity {
|
|||
public static final String DISPLAY_NAME = "display_name";
|
||||
public static final String HOSTNAME = "hostname";
|
||||
public static final String PORT = "port";
|
||||
public static final String STATUS = "status";
|
||||
public static final String STATUS_MESSAGE = "status_message";
|
||||
|
||||
public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
|
||||
|
||||
|
@ -48,9 +56,15 @@ public class Account extends AbstractEntity {
|
|||
public static final int OPTION_DISABLED = 1;
|
||||
public static final int OPTION_REGISTER = 2;
|
||||
public static final int OPTION_USECOMPRESSION = 3;
|
||||
public static final int OPTION_MAGIC_CREATE = 4;
|
||||
public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
|
||||
|
||||
public boolean httpUploadAvailable(long filesize) {
|
||||
return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
|
||||
}
|
||||
|
||||
public boolean httpUploadAvailable() {
|
||||
return xmppConnection != null && xmppConnection.getFeatures().httpUpload();
|
||||
return httpUploadAvailable(0);
|
||||
}
|
||||
|
||||
public void setDisplayName(String displayName) {
|
||||
|
@ -69,6 +83,29 @@ public class Account extends AbstractEntity {
|
|||
}
|
||||
}
|
||||
|
||||
public Contact getSelfContact() {
|
||||
return getRoster().getContact(jid);
|
||||
}
|
||||
|
||||
public boolean hasPendingPgpIntent(Conversation conversation) {
|
||||
return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
|
||||
}
|
||||
|
||||
public boolean isPgpDecryptionServiceConnected() {
|
||||
return pgpDecryptionService != null && pgpDecryptionService.isConnected();
|
||||
}
|
||||
|
||||
public boolean setShowErrorNotification(boolean newValue) {
|
||||
boolean oldValue = showErrorNotification();
|
||||
setKey("show_error",Boolean.toString(newValue));
|
||||
return newValue != oldValue;
|
||||
}
|
||||
|
||||
public boolean showErrorNotification() {
|
||||
String key = getKey("show_error");
|
||||
return key == null || Boolean.parseBoolean(key);
|
||||
}
|
||||
|
||||
public enum State {
|
||||
DISABLED,
|
||||
OFFLINE,
|
||||
|
@ -83,7 +120,15 @@ public class Account extends AbstractEntity {
|
|||
REGISTRATION_NOT_SUPPORTED(true),
|
||||
SECURITY_ERROR(true),
|
||||
INCOMPATIBLE_SERVER(true),
|
||||
TOR_NOT_AVAILABLE(true);
|
||||
TOR_NOT_AVAILABLE(true),
|
||||
BIND_FAILURE(true),
|
||||
HOST_UNKNOWN(true),
|
||||
REGISTRATION_PLEASE_WAIT(true),
|
||||
STREAM_ERROR(true),
|
||||
POLICY_VIOLATION(true),
|
||||
REGISTRATION_PASSWORD_TOO_WEAK(true),
|
||||
PAYMENT_REQUIRED(true),
|
||||
MISSING_INTERNET_PERMISSION(true);
|
||||
|
||||
private final boolean isError;
|
||||
|
||||
|
@ -91,11 +136,11 @@ public class Account extends AbstractEntity {
|
|||
return this.isError;
|
||||
}
|
||||
|
||||
private State(final boolean isError) {
|
||||
State(final boolean isError) {
|
||||
this.isError = isError;
|
||||
}
|
||||
|
||||
private State() {
|
||||
State() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
|
@ -129,6 +174,22 @@ public class Account extends AbstractEntity {
|
|||
return R.string.account_status_incompatible_server;
|
||||
case TOR_NOT_AVAILABLE:
|
||||
return R.string.account_status_tor_unavailable;
|
||||
case BIND_FAILURE:
|
||||
return R.string.account_status_bind_failure;
|
||||
case HOST_UNKNOWN:
|
||||
return R.string.account_status_host_unknown;
|
||||
case POLICY_VIOLATION:
|
||||
return R.string.account_status_policy_violation;
|
||||
case REGISTRATION_PLEASE_WAIT:
|
||||
return R.string.registration_please_wait;
|
||||
case REGISTRATION_PASSWORD_TOO_WEAK:
|
||||
return R.string.registration_password_too_weak;
|
||||
case STREAM_ERROR:
|
||||
return R.string.account_status_stream_error;
|
||||
case PAYMENT_REQUIRED:
|
||||
return R.string.payment_required;
|
||||
case MISSING_INTERNET_PERMISSION:
|
||||
return R.string.missing_internet_permission;
|
||||
default:
|
||||
return R.string.account_status_unknown;
|
||||
}
|
||||
|
@ -146,7 +207,7 @@ public class Account extends AbstractEntity {
|
|||
protected int options = 0;
|
||||
protected String rosterVersion;
|
||||
protected State status = State.OFFLINE;
|
||||
protected JSONObject keys = new JSONObject();
|
||||
protected final JSONObject keys;
|
||||
protected String avatar;
|
||||
protected String displayName = null;
|
||||
protected String hostname = null;
|
||||
|
@ -161,15 +222,18 @@ public class Account extends AbstractEntity {
|
|||
private final Roster roster = new Roster(this);
|
||||
private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
|
||||
private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
|
||||
private Presence.Status presenceStatus = Presence.Status.ONLINE;
|
||||
private String presenceStatusMessage = null;
|
||||
|
||||
public Account(final Jid jid, final String password) {
|
||||
this(java.util.UUID.randomUUID().toString(), jid,
|
||||
password, 0, null, "", null, null, null, 5222);
|
||||
password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
|
||||
}
|
||||
|
||||
private Account(final String uuid, final Jid jid,
|
||||
final String password, final int options, final String rosterVersion, final String keys,
|
||||
final String avatar, String displayName, String hostname, int port) {
|
||||
final String password, final int options, final String rosterVersion, final String keys,
|
||||
final String avatar, String displayName, String hostname, int port,
|
||||
final Presence.Status status, String statusMessage) {
|
||||
this.uuid = uuid;
|
||||
this.jid = jid;
|
||||
if (jid.isBareJid()) {
|
||||
|
@ -178,15 +242,19 @@ public class Account extends AbstractEntity {
|
|||
this.password = password;
|
||||
this.options = options;
|
||||
this.rosterVersion = rosterVersion;
|
||||
JSONObject tmp;
|
||||
try {
|
||||
this.keys = new JSONObject(keys);
|
||||
} catch (final JSONException ignored) {
|
||||
this.keys = new JSONObject();
|
||||
tmp = new JSONObject(keys);
|
||||
} catch(JSONException e) {
|
||||
tmp = new JSONObject();
|
||||
}
|
||||
this.keys = tmp;
|
||||
this.avatar = avatar;
|
||||
this.displayName = displayName;
|
||||
this.hostname = hostname;
|
||||
this.port = port;
|
||||
this.presenceStatus = status;
|
||||
this.presenceStatusMessage = statusMessage;
|
||||
}
|
||||
|
||||
public static Account fromCursor(final Cursor cursor) {
|
||||
|
@ -205,7 +273,9 @@ public class Account extends AbstractEntity {
|
|||
cursor.getString(cursor.getColumnIndex(AVATAR)),
|
||||
cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
|
||||
cursor.getString(cursor.getColumnIndex(HOSTNAME)),
|
||||
cursor.getInt(cursor.getColumnIndex(PORT)));
|
||||
cursor.getInt(cursor.getColumnIndex(PORT)),
|
||||
Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
|
||||
cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
|
||||
}
|
||||
|
||||
public boolean isOptionSet(final int option) {
|
||||
|
@ -224,8 +294,10 @@ public class Account extends AbstractEntity {
|
|||
return jid.getLocalpart();
|
||||
}
|
||||
|
||||
public void setJid(final Jid jid) {
|
||||
this.jid = jid;
|
||||
public boolean setJid(final Jid next) {
|
||||
final Jid prev = this.jid != null ? this.jid.toBareJid() : null;
|
||||
this.jid = next;
|
||||
return prev == null || (next != null && !prev.equals(next.toBareJid()));
|
||||
}
|
||||
|
||||
public Jid getServer() {
|
||||
|
@ -249,7 +321,8 @@ public class Account extends AbstractEntity {
|
|||
}
|
||||
|
||||
public boolean isOnion() {
|
||||
return getServer().toString().toLowerCase().endsWith(".onion");
|
||||
final Jid server = getServer();
|
||||
return server != null && server.toString().toLowerCase().endsWith(".onion");
|
||||
}
|
||||
|
||||
public void setPort(int port) {
|
||||
|
@ -268,6 +341,10 @@ public class Account extends AbstractEntity {
|
|||
}
|
||||
}
|
||||
|
||||
public State getTrueStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public void setStatus(final State status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
@ -277,7 +354,25 @@ public class Account extends AbstractEntity {
|
|||
}
|
||||
|
||||
public boolean hasErrorStatus() {
|
||||
return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 3;
|
||||
return getXmppConnection() != null
|
||||
&& (getStatus().isError() || getStatus() == State.CONNECTING)
|
||||
&& getXmppConnection().getAttempt() >= 3;
|
||||
}
|
||||
|
||||
public void setPresenceStatus(Presence.Status status) {
|
||||
this.presenceStatus = status;
|
||||
}
|
||||
|
||||
public Presence.Status getPresenceStatus() {
|
||||
return this.presenceStatus;
|
||||
}
|
||||
|
||||
public void setPresenceStatusMessage(String message) {
|
||||
this.presenceStatusMessage = message;
|
||||
}
|
||||
|
||||
public String getPresenceStatusMessage() {
|
||||
return this.presenceStatusMessage;
|
||||
}
|
||||
|
||||
public String getResource() {
|
||||
|
@ -306,15 +401,28 @@ public class Account extends AbstractEntity {
|
|||
}
|
||||
|
||||
public String getKey(final String name) {
|
||||
return this.keys.optString(name, null);
|
||||
synchronized (this.keys) {
|
||||
return this.keys.optString(name, null);
|
||||
}
|
||||
}
|
||||
|
||||
public int getKeyAsInt(final String name, int defaultValue) {
|
||||
String key = getKey(name);
|
||||
try {
|
||||
return key == null ? defaultValue : Integer.parseInt(key);
|
||||
} catch (NumberFormatException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setKey(final String keyName, final String keyValue) {
|
||||
try {
|
||||
this.keys.put(keyName, keyValue);
|
||||
return true;
|
||||
} catch (final JSONException e) {
|
||||
return false;
|
||||
synchronized (this.keys) {
|
||||
try {
|
||||
this.keys.put(keyName, keyValue);
|
||||
return true;
|
||||
} catch (final JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -334,12 +442,16 @@ public class Account extends AbstractEntity {
|
|||
values.put(SERVER, jid.getDomainpart());
|
||||
values.put(PASSWORD, password);
|
||||
values.put(OPTIONS, options);
|
||||
values.put(KEYS, this.keys.toString());
|
||||
synchronized (this.keys) {
|
||||
values.put(KEYS, this.keys.toString());
|
||||
}
|
||||
values.put(ROSTERVERSION, rosterVersion);
|
||||
values.put(AVATAR, avatar);
|
||||
values.put(DISPLAY_NAME, displayName);
|
||||
values.put(HOSTNAME, hostname);
|
||||
values.put(PORT, port);
|
||||
values.put(STATUS, presenceStatus.toShowString());
|
||||
values.put(STATUS_MESSAGE, presenceStatusMessage);
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -350,10 +462,10 @@ public class Account extends AbstractEntity {
|
|||
public void initAccountServices(final XmppConnectionService context) {
|
||||
this.mOtrService = new OtrService(context, this);
|
||||
this.axolotlService = new AxolotlService(this, context);
|
||||
this.pgpDecryptionService = new PgpDecryptionService(context);
|
||||
if (xmppConnection != null) {
|
||||
xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
|
||||
}
|
||||
this.pgpDecryptionService = new PgpDecryptionService(context);
|
||||
}
|
||||
|
||||
public OtrService getOtrService() {
|
||||
|
@ -361,7 +473,7 @@ public class Account extends AbstractEntity {
|
|||
}
|
||||
|
||||
public PgpDecryptionService getPgpDecryptionService() {
|
||||
return pgpDecryptionService;
|
||||
return this.pgpDecryptionService;
|
||||
}
|
||||
|
||||
public XmppConnection getXmppConnection() {
|
||||
|
@ -382,7 +494,7 @@ public class Account extends AbstractEntity {
|
|||
if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
|
||||
return null;
|
||||
}
|
||||
this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
|
||||
this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
|
||||
return this.otrFingerprint;
|
||||
} catch (final OtrCryptoException ignored) {
|
||||
return null;
|
||||
|
@ -405,58 +517,46 @@ public class Account extends AbstractEntity {
|
|||
}
|
||||
|
||||
public int countPresences() {
|
||||
return this.getRoster().getContact(this.getJid().toBareJid()).getPresences().size();
|
||||
return this.getSelfContact().getPresences().size();
|
||||
}
|
||||
|
||||
public String getPgpSignature() {
|
||||
try {
|
||||
if (keys.has(KEY_PGP_SIGNATURE) && !"null".equals(keys.getString(KEY_PGP_SIGNATURE))) {
|
||||
return keys.getString(KEY_PGP_SIGNATURE);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} catch (final JSONException e) {
|
||||
return null;
|
||||
}
|
||||
return getKey(KEY_PGP_SIGNATURE);
|
||||
}
|
||||
|
||||
public boolean setPgpSignature(String signature) {
|
||||
try {
|
||||
keys.put(KEY_PGP_SIGNATURE, signature);
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return setKey(KEY_PGP_SIGNATURE, signature);
|
||||
}
|
||||
|
||||
public boolean unsetPgpSignature() {
|
||||
try {
|
||||
keys.put(KEY_PGP_SIGNATURE, JSONObject.NULL);
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
synchronized (this.keys) {
|
||||
return keys.remove(KEY_PGP_SIGNATURE) != null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public long getPgpId() {
|
||||
if (keys.has(KEY_PGP_ID)) {
|
||||
try {
|
||||
return keys.getLong(KEY_PGP_ID);
|
||||
} catch (JSONException e) {
|
||||
return -1;
|
||||
synchronized (this.keys) {
|
||||
if (keys.has(KEY_PGP_ID)) {
|
||||
try {
|
||||
return keys.getLong(KEY_PGP_ID);
|
||||
} catch (JSONException e) {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setPgpSignId(long pgpID) {
|
||||
try {
|
||||
keys.put(KEY_PGP_ID, pgpID);
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
synchronized (this.keys) {
|
||||
try {
|
||||
keys.put(KEY_PGP_ID, pgpID);
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Roster getRoster() {
|
||||
|
@ -494,9 +594,8 @@ public class Account extends AbstractEntity {
|
|||
return this.avatar;
|
||||
}
|
||||
|
||||
public void activateGracePeriod() {
|
||||
this.mEndGracePeriod = SystemClock.elapsedRealtime()
|
||||
+ (Config.CARBON_GRACE_PERIOD * 1000);
|
||||
public void activateGracePeriod(long duration) {
|
||||
this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
|
||||
}
|
||||
|
||||
public void deactivateGracePeriod() {
|
||||
|
@ -508,14 +607,43 @@ public class Account extends AbstractEntity {
|
|||
}
|
||||
|
||||
public String getShareableUri() {
|
||||
final String fingerprint = this.getOtrFingerprint();
|
||||
if (fingerprint != null) {
|
||||
return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint;
|
||||
List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
|
||||
String uri = "xmpp:"+this.getJid().toBareJid().toString();
|
||||
if (fingerprints.size() > 0) {
|
||||
return XmppUri.getFingerprintUri(uri,fingerprints,';');
|
||||
} else {
|
||||
return "xmpp:" + this.getJid().toBareJid().toString();
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
public String getShareableLink() {
|
||||
List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
|
||||
String uri = "https://conversations.im/i/"+this.getJid().toBareJid().toString();
|
||||
if (fingerprints.size() > 0) {
|
||||
return XmppUri.getFingerprintUri(uri,fingerprints,'&');
|
||||
} else {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
|
||||
private List<XmppUri.Fingerprint> getFingerprints() {
|
||||
ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
|
||||
final String otr = this.getOtrFingerprint();
|
||||
if (otr != null) {
|
||||
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR,otr));
|
||||
}
|
||||
if (axolotlService == null) {
|
||||
return fingerprints;
|
||||
}
|
||||
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,axolotlService.getOwnFingerprint().substring(2),axolotlService.getOwnDeviceId()));
|
||||
for(XmppAxolotlSession session : axolotlService.findOwnSessions()) {
|
||||
if (session.getTrust().isVerified() && session.getTrust().isActive()) {
|
||||
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,session.getFingerprint().substring(2).replaceAll("\\s",""),session.getRemoteAddress().getDeviceId()));
|
||||
}
|
||||
}
|
||||
return fingerprints;
|
||||
}
|
||||
|
||||
public boolean isBlocked(final ListItem contact) {
|
||||
final Jid jid = contact.getJid();
|
||||
return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
@ -47,13 +50,25 @@ public class Bookmark extends Element implements ListItem {
|
|||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
if (this.mJoinedConversation != null
|
||||
&& (this.mJoinedConversation.getMucOptions().getSubject() != null)) {
|
||||
return this.mJoinedConversation.getMucOptions().getSubject();
|
||||
} else if (getBookmarkName() != null) {
|
||||
return getBookmarkName();
|
||||
if (this.mJoinedConversation != null) {
|
||||
return this.mJoinedConversation.getName();
|
||||
} else if (getBookmarkName() != null
|
||||
&& !getBookmarkName().trim().isEmpty()) {
|
||||
return getBookmarkName().trim();
|
||||
} else {
|
||||
return this.getJid().getLocalpart();
|
||||
Jid jid = this.getJid();
|
||||
String name = jid != null ? jid.getLocalpart() : getAttribute("jid");
|
||||
return name != null ? name : "";
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayJid() {
|
||||
Jid jid = getJid();
|
||||
if (jid != null) {
|
||||
return jid.toString();
|
||||
} else {
|
||||
return getAttribute("jid"); //fallback if jid wasn't parsable
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -63,8 +78,8 @@ public class Bookmark extends Element implements ListItem {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<Tag> getTags() {
|
||||
ArrayList<Tag> tags = new ArrayList<Tag>();
|
||||
public List<Tag> getTags(Context context) {
|
||||
ArrayList<Tag> tags = new ArrayList<>();
|
||||
for (Element element : getChildren()) {
|
||||
if (element.getName().equals("group") && element.getContent() != null) {
|
||||
String group = element.getContent();
|
||||
|
@ -101,7 +116,8 @@ public class Bookmark extends Element implements ListItem {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean match(String needle) {
|
||||
@Override
|
||||
public boolean match(Context context, String needle) {
|
||||
if (needle == null) {
|
||||
return true;
|
||||
}
|
||||
|
@ -109,12 +125,12 @@ public class Bookmark extends Element implements ListItem {
|
|||
final Jid jid = getJid();
|
||||
return (jid != null && jid.toString().contains(needle)) ||
|
||||
getDisplayName().toLowerCase(Locale.US).contains(needle) ||
|
||||
matchInTag(needle);
|
||||
matchInTag(context, needle);
|
||||
}
|
||||
|
||||
private boolean matchInTag(String needle) {
|
||||
private boolean matchInTag(Context context, String needle) {
|
||||
needle = needle.toLowerCase(Locale.US);
|
||||
for (Tag tag : getTags()) {
|
||||
for (Tag tag : getTags(context)) {
|
||||
if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
@ -33,24 +36,29 @@ public class Contact implements ListItem, Blockable {
|
|||
public static final String LAST_PRESENCE = "last_presence";
|
||||
public static final String LAST_TIME = "last_time";
|
||||
public static final String GROUPS = "groups";
|
||||
public Lastseen lastseen = new Lastseen();
|
||||
protected String accountUuid;
|
||||
protected String systemName;
|
||||
protected String serverName;
|
||||
protected String presenceName;
|
||||
protected String commonName;
|
||||
protected Jid jid;
|
||||
protected int subscription = 0;
|
||||
protected String systemAccount;
|
||||
protected String photoUri;
|
||||
protected JSONObject keys = new JSONObject();
|
||||
protected JSONArray groups = new JSONArray();
|
||||
protected Presences presences = new Presences();
|
||||
protected final Presences presences = new Presences();
|
||||
protected Account account;
|
||||
protected Avatar avatar;
|
||||
|
||||
private boolean mActive = false;
|
||||
private long mLastseen = 0;
|
||||
private String mLastPresence = null;
|
||||
|
||||
public Contact(final String account, final String systemName, final String serverName,
|
||||
final Jid jid, final int subscription, final String photoUri,
|
||||
final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) {
|
||||
final String systemAccount, final String keys, final String avatar, final long lastseen,
|
||||
final String presence, final String groups) {
|
||||
this.accountUuid = account;
|
||||
this.systemName = systemName;
|
||||
this.serverName = serverName;
|
||||
|
@ -73,7 +81,8 @@ public class Contact implements ListItem, Blockable {
|
|||
} catch (JSONException e) {
|
||||
this.groups = new JSONArray();
|
||||
}
|
||||
this.lastseen = lastseen;
|
||||
this.mLastseen = lastseen;
|
||||
this.mLastPresence = presence;
|
||||
}
|
||||
|
||||
public Contact(final Jid jid) {
|
||||
|
@ -81,9 +90,6 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
|
||||
public static Contact fromCursor(final Cursor cursor) {
|
||||
final Lastseen lastseen = new Lastseen(
|
||||
cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
|
||||
cursor.getLong(cursor.getColumnIndex(LAST_TIME)));
|
||||
final Jid jid;
|
||||
try {
|
||||
jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true);
|
||||
|
@ -100,26 +106,36 @@ public class Contact implements ListItem, Blockable {
|
|||
cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
|
||||
cursor.getString(cursor.getColumnIndex(KEYS)),
|
||||
cursor.getString(cursor.getColumnIndex(AVATAR)),
|
||||
lastseen,
|
||||
cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
|
||||
cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
|
||||
cursor.getString(cursor.getColumnIndex(GROUPS)));
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
if (this.presenceName != null && Config.X509_VERIFICATION) {
|
||||
return this.presenceName;
|
||||
if (this.commonName != null && Config.X509_VERIFICATION) {
|
||||
return this.commonName;
|
||||
} else if (this.systemName != null) {
|
||||
return this.systemName;
|
||||
} else if (this.serverName != null) {
|
||||
return this.serverName;
|
||||
} else if (this.presenceName != null) {
|
||||
} else if (this.presenceName != null && mutualPresenceSubscription()) {
|
||||
return this.presenceName;
|
||||
} else if (jid.hasLocalpart()) {
|
||||
return jid.getLocalpart();
|
||||
return jid.getUnescapedLocalpart();
|
||||
} else {
|
||||
return jid.getDomainpart();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayJid() {
|
||||
if (jid != null) {
|
||||
return jid.toString();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getProfilePhoto() {
|
||||
return this.photoUri;
|
||||
}
|
||||
|
@ -129,25 +145,14 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
|
||||
@Override
|
||||
public List<Tag> getTags() {
|
||||
public List<Tag> getTags(Context context) {
|
||||
final ArrayList<Tag> tags = new ArrayList<>();
|
||||
for (final String group : getGroups()) {
|
||||
tags.add(new Tag(group, UIHelper.getColorForName(group)));
|
||||
}
|
||||
switch (getMostAvailableStatus()) {
|
||||
case Presences.CHAT:
|
||||
case Presences.ONLINE:
|
||||
tags.add(new Tag("online", 0xff259b24));
|
||||
break;
|
||||
case Presences.AWAY:
|
||||
tags.add(new Tag("away", 0xffff9800));
|
||||
break;
|
||||
case Presences.XA:
|
||||
tags.add(new Tag("not available", 0xfff44336));
|
||||
break;
|
||||
case Presences.DND:
|
||||
tags.add(new Tag("dnd", 0xfff44336));
|
||||
break;
|
||||
Presence.Status status = getShownStatus();
|
||||
if (status != Presence.Status.OFFLINE) {
|
||||
tags.add(UIHelper.getTagForStatus(context, status));
|
||||
}
|
||||
if (isBlocked()) {
|
||||
tags.add(new Tag("blocked", 0xff2e2f3b));
|
||||
|
@ -155,7 +160,7 @@ public class Contact implements ListItem, Blockable {
|
|||
return tags;
|
||||
}
|
||||
|
||||
public boolean match(String needle) {
|
||||
public boolean match(Context context, String needle) {
|
||||
if (needle == null || needle.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -163,7 +168,7 @@ public class Contact implements ListItem, Blockable {
|
|||
String[] parts = needle.split("\\s+");
|
||||
if (parts.length > 1) {
|
||||
for(int i = 0; i < parts.length; ++i) {
|
||||
if (!match(parts[i])) {
|
||||
if (!match(context, parts[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -171,13 +176,13 @@ public class Contact implements ListItem, Blockable {
|
|||
} else {
|
||||
return jid.toString().contains(needle) ||
|
||||
getDisplayName().toLowerCase(Locale.US).contains(needle) ||
|
||||
matchInTag(needle);
|
||||
matchInTag(context, needle);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchInTag(String needle) {
|
||||
private boolean matchInTag(Context context, String needle) {
|
||||
needle = needle.toLowerCase(Locale.US);
|
||||
for (Tag tag : getTags()) {
|
||||
for (Tag tag : getTags(context)) {
|
||||
if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -191,23 +196,19 @@ public class Contact implements ListItem, Blockable {
|
|||
values.put(ACCOUNT, accountUuid);
|
||||
values.put(SYSTEMNAME, systemName);
|
||||
values.put(SERVERNAME, serverName);
|
||||
values.put(JID, jid.toString());
|
||||
values.put(JID, jid.toPreppedString());
|
||||
values.put(OPTIONS, subscription);
|
||||
values.put(SYSTEMACCOUNT, systemAccount);
|
||||
values.put(PHOTOURI, photoUri);
|
||||
values.put(KEYS, keys.toString());
|
||||
values.put(AVATAR, avatar == null ? null : avatar.getFilename());
|
||||
values.put(LAST_PRESENCE, lastseen.presence);
|
||||
values.put(LAST_TIME, lastseen.time);
|
||||
values.put(LAST_PRESENCE, mLastPresence);
|
||||
values.put(LAST_TIME, mLastseen);
|
||||
values.put(GROUPS, groups.toString());
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
public int getSubscription() {
|
||||
return this.subscription;
|
||||
}
|
||||
|
||||
public Account getAccount() {
|
||||
return this.account;
|
||||
}
|
||||
|
@ -221,12 +222,8 @@ public class Contact implements ListItem, Blockable {
|
|||
return this.presences;
|
||||
}
|
||||
|
||||
public void setPresences(Presences pres) {
|
||||
this.presences = pres;
|
||||
}
|
||||
|
||||
public void updatePresence(final String resource, final int status) {
|
||||
this.presences.updatePresence(resource, status);
|
||||
public void updatePresence(final String resource, final Presence presence) {
|
||||
this.presences.updatePresence(resource, presence);
|
||||
}
|
||||
|
||||
public void removePresence(final String resource) {
|
||||
|
@ -238,8 +235,8 @@ public class Contact implements ListItem, Blockable {
|
|||
this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
|
||||
}
|
||||
|
||||
public int getMostAvailableStatus() {
|
||||
return this.presences.getMostAvailableStatus();
|
||||
public Presence.Status getShownStatus() {
|
||||
return this.presences.getShownStatus();
|
||||
}
|
||||
|
||||
public boolean setPhotoUri(String uri) {
|
||||
|
@ -266,8 +263,18 @@ public class Contact implements ListItem, Blockable {
|
|||
this.presenceName = presenceName;
|
||||
}
|
||||
|
||||
public String getSystemAccount() {
|
||||
return systemAccount;
|
||||
public Uri getSystemAccount() {
|
||||
if (systemAccount == null) {
|
||||
return null;
|
||||
} else {
|
||||
String[] parts = systemAccount.split("#");
|
||||
if (parts.length != 2) {
|
||||
return null;
|
||||
} else {
|
||||
long id = Long.parseLong(parts[0]);
|
||||
return ContactsContract.Contacts.getLookupUri(id, parts[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setSystemAccount(String account) {
|
||||
|
@ -294,7 +301,7 @@ public class Contact implements ListItem, Blockable {
|
|||
for (int i = 0; i < prints.length(); ++i) {
|
||||
final String print = prints.isNull(i) ? null : prints.getString(i);
|
||||
if (print != null && !print.isEmpty()) {
|
||||
fingerprints.add(prints.getString(i));
|
||||
fingerprints.add(prints.getString(i).toLowerCase(Locale.US));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -380,11 +387,13 @@ public class Contact implements ListItem, Blockable {
|
|||
this.resetOption(Options.TO);
|
||||
this.setOption(Options.FROM);
|
||||
this.resetOption(Options.PREEMPTIVE_GRANT);
|
||||
this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
|
||||
break;
|
||||
case "both":
|
||||
this.setOption(Options.TO);
|
||||
this.setOption(Options.FROM);
|
||||
this.resetOption(Options.PREEMPTIVE_GRANT);
|
||||
this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
|
||||
break;
|
||||
case "none":
|
||||
this.resetOption(Options.FROM);
|
||||
|
@ -474,19 +483,10 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean trusted() {
|
||||
public boolean mutualPresenceSubscription() {
|
||||
return getOption(Options.FROM) && getOption(Options.TO);
|
||||
}
|
||||
|
||||
public String getShareableUri() {
|
||||
if (getOtrFingerprints().size() >= 1) {
|
||||
String otr = getOtrFingerprints().get(0);
|
||||
return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
|
||||
} else {
|
||||
return "xmpp:" + getJid().toBareJid().toString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlocked() {
|
||||
return getAccount().isBlocked(this);
|
||||
|
@ -510,18 +510,36 @@ public class Contact implements ListItem, Blockable {
|
|||
return account.getJid().toBareJid().equals(getJid().toBareJid());
|
||||
}
|
||||
|
||||
public static class Lastseen {
|
||||
public long time;
|
||||
public String presence;
|
||||
public void setCommonName(String cn) {
|
||||
this.commonName = cn;
|
||||
}
|
||||
|
||||
public Lastseen() {
|
||||
this(null, 0);
|
||||
}
|
||||
public void flagActive() {
|
||||
this.mActive = true;
|
||||
}
|
||||
|
||||
public Lastseen(final String presence, final long time) {
|
||||
this.presence = presence;
|
||||
this.time = time;
|
||||
}
|
||||
public void flagInactive() {
|
||||
this.mActive = false;
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return this.mActive;
|
||||
}
|
||||
|
||||
public void setLastseen(long timestamp) {
|
||||
this.mLastseen = Math.max(timestamp, mLastseen);
|
||||
}
|
||||
|
||||
public long getLastseen() {
|
||||
return this.mLastseen;
|
||||
}
|
||||
|
||||
public void setLastResource(String resource) {
|
||||
this.mLastPresence = resource;
|
||||
}
|
||||
|
||||
public String getLastResource() {
|
||||
return this.mLastPresence;
|
||||
}
|
||||
|
||||
public final class Options {
|
||||
|
|
|
@ -9,23 +9,30 @@ import net.java.otr4j.session.SessionID;
|
|||
import net.java.otr4j.session.SessionImpl;
|
||||
import net.java.otr4j.session.SessionStatus;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.security.interfaces.DSAPublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.PgpDecryptionService;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
||||
public class Conversation extends AbstractEntity implements Blockable {
|
||||
|
||||
public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation> {
|
||||
public static final String TABLENAME = "conversations";
|
||||
|
||||
public static final int STATUS_AVAILABLE = 0;
|
||||
|
@ -48,6 +55,8 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
|
||||
public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
|
||||
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
|
||||
public static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
|
||||
public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
|
||||
|
||||
private String name;
|
||||
private String contactUuid;
|
||||
|
@ -81,6 +90,9 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
|
||||
private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
|
||||
private String mLastReceivedOtrMessageId = null;
|
||||
private String mFirstMamReference = null;
|
||||
private Message correctingMessage;
|
||||
public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
|
||||
|
||||
public boolean hasMessagesLeftOnServer() {
|
||||
return messagesLeftOnServer;
|
||||
|
@ -90,6 +102,21 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
this.messagesLeftOnServer = value;
|
||||
}
|
||||
|
||||
|
||||
public Message getFirstUnreadMessage() {
|
||||
Message first = null;
|
||||
synchronized (this.messages) {
|
||||
for (int i = messages.size() - 1; i >= 0; --i) {
|
||||
if (messages.get(i).isRead()) {
|
||||
return first;
|
||||
} else {
|
||||
first = messages.get(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return first;
|
||||
}
|
||||
|
||||
public Message findUnsentMessageWithUuid(String uuid) {
|
||||
synchronized(this.messages) {
|
||||
for (final Message message : this.messages) {
|
||||
|
@ -136,9 +163,9 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
public Message findMessageWithFileAndUuid(final String uuid) {
|
||||
synchronized (this.messages) {
|
||||
for (final Message message : this.messages) {
|
||||
if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
|
||||
if (message.getUuid().equals(uuid)
|
||||
&& message.getEncryption() != Message.ENCRYPTION_PGP
|
||||
&& message.getUuid().equals(uuid)) {
|
||||
&& (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.treatAsDownloadable() != Message.Decision.NEVER)) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +212,13 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
final int size = messages.size();
|
||||
final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
|
||||
if (size > maxsize) {
|
||||
this.messages.subList(0, size - maxsize).clear();
|
||||
List<Message> discards = this.messages.subList(0, size - maxsize);
|
||||
final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
|
||||
if (pgpDecryptionService != null) {
|
||||
pgpDecryptionService.discard(discards);
|
||||
}
|
||||
discards.clear();
|
||||
untieMessages();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -225,6 +258,24 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
return null;
|
||||
}
|
||||
|
||||
public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
|
||||
synchronized (this.messages) {
|
||||
for(int i = this.messages.size() - 1; i >= 0; --i) {
|
||||
Message message = messages.get(i);
|
||||
if (counterpart.equals(message.getCounterpart())
|
||||
&& ((message.getStatus() == Message.STATUS_RECEIVED) == received)
|
||||
&& (carbon == message.isCarbon() || received) ) {
|
||||
if (id.equals(message.getRemoteMsgId())) {
|
||||
return message;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Message findSentMessageWithUuid(String id) {
|
||||
synchronized (this.messages) {
|
||||
for (Message message : this.messages) {
|
||||
|
@ -277,6 +328,59 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
}
|
||||
}
|
||||
|
||||
public void setFirstMamReference(String reference) {
|
||||
this.mFirstMamReference = reference;
|
||||
}
|
||||
|
||||
public String getFirstMamReference() {
|
||||
return this.mFirstMamReference;
|
||||
}
|
||||
|
||||
public void setLastClearHistory(long time) {
|
||||
setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY,String.valueOf(time));
|
||||
}
|
||||
|
||||
public long getLastClearHistory() {
|
||||
return getLongAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, 0);
|
||||
}
|
||||
|
||||
public List<Jid> getAcceptedCryptoTargets() {
|
||||
if (mode == MODE_SINGLE) {
|
||||
return Arrays.asList(getJid().toBareJid());
|
||||
} else {
|
||||
return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
|
||||
}
|
||||
}
|
||||
|
||||
public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
|
||||
setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
|
||||
}
|
||||
|
||||
public void setCorrectingMessage(Message correctingMessage) {
|
||||
this.correctingMessage = correctingMessage;
|
||||
}
|
||||
|
||||
public Message getCorrectingMessage() {
|
||||
return this.correctingMessage;
|
||||
}
|
||||
|
||||
public boolean withSelf() {
|
||||
return getContact().isSelf();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Conversation another) {
|
||||
final Message left = getLatestMessage();
|
||||
final Message right = another.getLatestMessage();
|
||||
if (left.getTimeSent() > right.getTimeSent()) {
|
||||
return -1;
|
||||
} else if (left.getTimeSent() < right.getTimeSent()) {
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public interface OnMessageFound {
|
||||
void onMessageFound(final Message message);
|
||||
}
|
||||
|
@ -354,14 +458,16 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
if (getMode() == MODE_MULTI) {
|
||||
if (getMucOptions().getSubject() != null) {
|
||||
return getMucOptions().getSubject();
|
||||
} else if (bookmark != null && bookmark.getBookmarkName() != null) {
|
||||
return bookmark.getBookmarkName();
|
||||
} else if (bookmark != null
|
||||
&& bookmark.getBookmarkName() != null
|
||||
&& !bookmark.getBookmarkName().trim().isEmpty()) {
|
||||
return bookmark.getBookmarkName().trim();
|
||||
} else {
|
||||
String generatedName = getMucOptions().createNameFromParticipants();
|
||||
if (generatedName != null) {
|
||||
return generatedName;
|
||||
} else {
|
||||
return getJid().getLocalpart();
|
||||
return getJid().getUnescapedLocalpart();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -404,7 +510,7 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
values.put(NAME, name);
|
||||
values.put(CONTACT, contactUuid);
|
||||
values.put(ACCOUNT, accountUuid);
|
||||
values.put(CONTACTJID, contactJid.toString());
|
||||
values.put(CONTACTJID, contactJid.toPreppedString());
|
||||
values.put(CREATED, created);
|
||||
values.put(STATUS, status);
|
||||
values.put(MODE, mode);
|
||||
|
@ -480,15 +586,18 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
return mSmp;
|
||||
}
|
||||
|
||||
public void startOtrIfNeeded() {
|
||||
if (this.otrSession != null
|
||||
&& this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
|
||||
public boolean startOtrIfNeeded() {
|
||||
if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
|
||||
try {
|
||||
this.otrSession.startSession();
|
||||
return true;
|
||||
} catch (OtrException e) {
|
||||
this.resetOtrSession();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean endOtrIfNeeded() {
|
||||
|
@ -522,7 +631,7 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
return null;
|
||||
}
|
||||
DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
|
||||
this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
|
||||
this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
|
||||
} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
|
||||
return null;
|
||||
}
|
||||
|
@ -574,66 +683,41 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
return this.nextCounterpart;
|
||||
}
|
||||
|
||||
private int getMostRecentlyUsedOutgoingEncryption() {
|
||||
synchronized (this.messages) {
|
||||
for(int i = this.messages.size() -1; i >= 0; --i) {
|
||||
final Message m = this.messages.get(i);
|
||||
if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
|
||||
final int e = m.getEncryption();
|
||||
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
|
||||
return Message.ENCRYPTION_PGP;
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Message.ENCRYPTION_NONE;
|
||||
}
|
||||
|
||||
private int getMostRecentlyUsedIncomingEncryption() {
|
||||
synchronized (this.messages) {
|
||||
for(int i = this.messages.size() -1; i >= 0; --i) {
|
||||
final Message m = this.messages.get(i);
|
||||
if (m.getStatus() == Message.STATUS_RECEIVED) {
|
||||
final int e = m.getEncryption();
|
||||
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
|
||||
return Message.ENCRYPTION_PGP;
|
||||
} else {
|
||||
return e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Message.ENCRYPTION_NONE;
|
||||
}
|
||||
|
||||
public int getNextEncryption() {
|
||||
final AxolotlService axolotlService = getAccount().getAxolotlService();
|
||||
int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
|
||||
if (next == -1) {
|
||||
if (Config.X509_VERIFICATION && mode == MODE_SINGLE) {
|
||||
if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
|
||||
return Message.ENCRYPTION_AXOLOTL;
|
||||
} else {
|
||||
return Message.ENCRYPTION_NONE;
|
||||
}
|
||||
}
|
||||
int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
|
||||
if (outgoing == Message.ENCRYPTION_NONE) {
|
||||
next = this.getMostRecentlyUsedIncomingEncryption();
|
||||
} else {
|
||||
next = outgoing;
|
||||
}
|
||||
return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
|
||||
}
|
||||
|
||||
private int fixAvailableEncryption(int selectedEncryption) {
|
||||
switch(selectedEncryption) {
|
||||
case Message.ENCRYPTION_NONE:
|
||||
return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
|
||||
case Message.ENCRYPTION_AXOLOTL:
|
||||
return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
|
||||
case Message.ENCRYPTION_OTR:
|
||||
return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
|
||||
case Message.ENCRYPTION_PGP:
|
||||
case Message.ENCRYPTION_DECRYPTED:
|
||||
case Message.ENCRYPTION_DECRYPTION_FAILED:
|
||||
return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
|
||||
default:
|
||||
return getDefaultEncryption();
|
||||
}
|
||||
if (Config.FORCE_E2E_ENCRYPTION && mode == MODE_SINGLE && next <= 0) {
|
||||
if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
|
||||
return Message.ENCRYPTION_AXOLOTL;
|
||||
} else {
|
||||
return Message.ENCRYPTION_OTR;
|
||||
}
|
||||
}
|
||||
|
||||
private int getDefaultEncryption() {
|
||||
AxolotlService axolotlService = account.getAxolotlService();
|
||||
if (Config.supportUnencrypted()) {
|
||||
return Message.ENCRYPTION_NONE;
|
||||
} else if (Config.supportOmemo()
|
||||
&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
|
||||
return Message.ENCRYPTION_AXOLOTL;
|
||||
} else if (Config.supportOtr() && mode == MODE_SINGLE) {
|
||||
return Message.ENCRYPTION_OTR;
|
||||
} else if (Config.supportOpenPgp()) {
|
||||
return Message.ENCRYPTION_PGP;
|
||||
} else {
|
||||
return Message.ENCRYPTION_NONE;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
public void setNextEncryption(int encryption) {
|
||||
|
@ -682,7 +766,7 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
public boolean hasDuplicateMessage(Message message) {
|
||||
synchronized (this.messages) {
|
||||
for (int i = this.messages.size() - 1; i >= 0; --i) {
|
||||
if (this.messages.get(i).equals(message)) {
|
||||
if (this.messages.get(i).similar(message)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -711,15 +795,18 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
}
|
||||
|
||||
public long getLastMessageTransmitted() {
|
||||
final long last_clear = getLastClearHistory();
|
||||
long last_received = 0;
|
||||
synchronized (this.messages) {
|
||||
for(int i = this.messages.size() - 1; i >= 0; --i) {
|
||||
Message message = this.messages.get(i);
|
||||
if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
|
||||
return message.getTimeSent();
|
||||
last_received = message.getTimeSent();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
return Math.max(last_clear,last_received);
|
||||
}
|
||||
|
||||
public void setMutedTill(long value) {
|
||||
|
@ -731,26 +818,65 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
}
|
||||
|
||||
public boolean alwaysNotify() {
|
||||
return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY,Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
|
||||
return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
|
||||
}
|
||||
|
||||
public boolean setAttribute(String key, String value) {
|
||||
try {
|
||||
this.attributes.put(key, value);
|
||||
return true;
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
synchronized (this.attributes) {
|
||||
try {
|
||||
this.attributes.put(key, value);
|
||||
return true;
|
||||
} catch (JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setAttribute(String key, List<Jid> jids) {
|
||||
JSONArray array = new JSONArray();
|
||||
for(Jid jid : jids) {
|
||||
array.put(jid.toBareJid().toString());
|
||||
}
|
||||
synchronized (this.attributes) {
|
||||
try {
|
||||
this.attributes.put(key, array);
|
||||
return true;
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public String getAttribute(String key) {
|
||||
try {
|
||||
return this.attributes.getString(key);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
synchronized (this.attributes) {
|
||||
try {
|
||||
return this.attributes.getString(key);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Jid> getJidListAttribute(String key) {
|
||||
ArrayList<Jid> list = new ArrayList<>();
|
||||
synchronized (this.attributes) {
|
||||
try {
|
||||
JSONArray array = this.attributes.getJSONArray(key);
|
||||
for (int i = 0; i < array.length(); ++i) {
|
||||
try {
|
||||
list.add(Jid.fromString(array.getString(i)));
|
||||
} catch (InvalidJidException e) {
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
public int getIntAttribute(String key, int defaultValue) {
|
||||
String value = this.getAttribute(key);
|
||||
if (value == null) {
|
||||
|
@ -793,11 +919,29 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
}
|
||||
}
|
||||
|
||||
public void prepend(Message message) {
|
||||
message.setConversation(this);
|
||||
synchronized (this.messages) {
|
||||
this.messages.add(0,message);
|
||||
}
|
||||
}
|
||||
|
||||
public void addAll(int index, List<Message> messages) {
|
||||
synchronized (this.messages) {
|
||||
this.messages.addAll(index, messages);
|
||||
}
|
||||
account.getPgpDecryptionService().addAll(messages);
|
||||
account.getPgpDecryptionService().decrypt(messages);
|
||||
}
|
||||
|
||||
public void expireOldMessages(long timestamp) {
|
||||
synchronized (this.messages) {
|
||||
for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
|
||||
if (iterator.next().getTimeSent() < timestamp) {
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
untieMessages();
|
||||
}
|
||||
}
|
||||
|
||||
public void sort() {
|
||||
|
@ -814,9 +958,13 @@ public class Conversation extends AbstractEntity implements Blockable {
|
|||
}
|
||||
}
|
||||
});
|
||||
for(Message message : this.messages) {
|
||||
message.untie();
|
||||
}
|
||||
untieMessages();
|
||||
}
|
||||
}
|
||||
|
||||
private void untieMessages() {
|
||||
for(Message message : this.messages) {
|
||||
message.untie();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,20 +52,16 @@ public class DownloadableFile extends File {
|
|||
|
||||
public void setKeyAndIv(byte[] keyIvCombo) {
|
||||
if (keyIvCombo.length == 48) {
|
||||
byte[] secretKey = new byte[32];
|
||||
byte[] iv = new byte[16];
|
||||
System.arraycopy(keyIvCombo, 0, iv, 0, 16);
|
||||
System.arraycopy(keyIvCombo, 16, secretKey, 0, 32);
|
||||
this.aeskey = secretKey;
|
||||
this.iv = iv;
|
||||
this.aeskey = new byte[32];
|
||||
this.iv = new byte[16];
|
||||
System.arraycopy(keyIvCombo, 0, this.iv, 0, 16);
|
||||
System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32);
|
||||
} else if (keyIvCombo.length >= 32) {
|
||||
byte[] secretKey = new byte[32];
|
||||
System.arraycopy(keyIvCombo, 0, secretKey, 0, 32);
|
||||
this.aeskey = secretKey;
|
||||
this.aeskey = new byte[32];
|
||||
System.arraycopy(keyIvCombo, 0, aeskey, 0, 32);
|
||||
} else if (keyIvCombo.length >= 16) {
|
||||
byte[] secretKey = new byte[16];
|
||||
System.arraycopy(keyIvCombo, 0, secretKey, 0, 16);
|
||||
this.aeskey = secretKey;
|
||||
this.aeskey = new byte[16];
|
||||
System.arraycopy(keyIvCombo, 0, this.aeskey, 0, 16);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
||||
public interface ListItem extends Comparable<ListItem> {
|
||||
public String getDisplayName();
|
||||
String getDisplayName();
|
||||
|
||||
public Jid getJid();
|
||||
String getDisplayJid();
|
||||
|
||||
public List<Tag> getTags();
|
||||
Jid getJid();
|
||||
|
||||
public final class Tag {
|
||||
List<Tag> getTags(Context context);
|
||||
|
||||
final class Tag {
|
||||
private final String name;
|
||||
private final int color;
|
||||
|
||||
|
@ -29,5 +33,5 @@ public interface ListItem extends Comparable<ListItem> {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean match(final String needle);
|
||||
boolean match(Context context, final String needle);
|
||||
}
|
||||
|
|
|
@ -2,13 +2,15 @@ package eu.siacs.conversations.entities;
|
|||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.GeoHelper;
|
||||
import eu.siacs.conversations.utils.MimeUtils;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
|
@ -19,8 +21,6 @@ public class Message extends AbstractEntity {
|
|||
|
||||
public static final String TABLENAME = "messages";
|
||||
|
||||
public static final String MERGE_SEPARATOR = " \u200B\n\n";
|
||||
|
||||
public static final int STATUS_RECEIVED = 0;
|
||||
public static final int STATUS_UNSEND = 1;
|
||||
public static final int STATUS_SEND = 2;
|
||||
|
@ -52,11 +52,14 @@ public class Message extends AbstractEntity {
|
|||
public static final String STATUS = "status";
|
||||
public static final String TYPE = "type";
|
||||
public static final String CARBON = "carbon";
|
||||
public static final String OOB = "oob";
|
||||
public static final String EDITED = "edited";
|
||||
public static final String REMOTE_MSG_ID = "remoteMsgId";
|
||||
public static final String SERVER_MSG_ID = "serverMsgId";
|
||||
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
|
||||
public static final String FINGERPRINT = "axolotl_fingerprint";
|
||||
public static final String READ = "read";
|
||||
public static final String ERROR_MESSAGE = "errorMsg";
|
||||
public static final String ME_COMMAND = "/me ";
|
||||
|
||||
|
||||
|
@ -71,6 +74,8 @@ public class Message extends AbstractEntity {
|
|||
protected int status;
|
||||
protected int type;
|
||||
protected boolean carbon = false;
|
||||
protected boolean oob = false;
|
||||
protected String edited = null;
|
||||
protected String relativeFilePath;
|
||||
protected boolean read = true;
|
||||
protected String remoteMsgId = null;
|
||||
|
@ -80,6 +85,7 @@ public class Message extends AbstractEntity {
|
|||
private Message mNextMessage = null;
|
||||
private Message mPreviousMessage = null;
|
||||
private String axolotlFingerprint = null;
|
||||
private String errorMessage = null;
|
||||
|
||||
private Message() {
|
||||
|
||||
|
@ -104,7 +110,10 @@ public class Message extends AbstractEntity {
|
|||
null,
|
||||
null,
|
||||
null,
|
||||
true);
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
null);
|
||||
this.conversation = conversation;
|
||||
}
|
||||
|
||||
|
@ -112,12 +121,13 @@ public class Message extends AbstractEntity {
|
|||
final Jid trueCounterpart, final String body, final long timeSent,
|
||||
final int encryption, final int status, final int type, final boolean carbon,
|
||||
final String remoteMsgId, final String relativeFilePath,
|
||||
final String serverMsgId, final String fingerprint, final boolean read) {
|
||||
final String serverMsgId, final String fingerprint, final boolean read,
|
||||
final String edited, final boolean oob, final String errorMessage) {
|
||||
this.uuid = uuid;
|
||||
this.conversationUuid = conversationUUid;
|
||||
this.counterpart = counterpart;
|
||||
this.trueCounterpart = trueCounterpart;
|
||||
this.body = body;
|
||||
this.body = body == null ? "" : body;
|
||||
this.timeSent = timeSent;
|
||||
this.encryption = encryption;
|
||||
this.status = status;
|
||||
|
@ -128,6 +138,9 @@ public class Message extends AbstractEntity {
|
|||
this.serverMsgId = serverMsgId;
|
||||
this.axolotlFingerprint = fingerprint;
|
||||
this.read = read;
|
||||
this.edited = edited;
|
||||
this.oob = oob;
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
|
||||
public static Message fromCursor(Cursor cursor) {
|
||||
|
@ -162,12 +175,15 @@ public class Message extends AbstractEntity {
|
|||
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
|
||||
cursor.getInt(cursor.getColumnIndex(STATUS)),
|
||||
cursor.getInt(cursor.getColumnIndex(TYPE)),
|
||||
cursor.getInt(cursor.getColumnIndex(CARBON))>0,
|
||||
cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
|
||||
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
|
||||
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
|
||||
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
|
||||
cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
|
||||
cursor.getInt(cursor.getColumnIndex(READ)) > 0);
|
||||
cursor.getInt(cursor.getColumnIndex(READ)) > 0,
|
||||
cursor.getString(cursor.getColumnIndex(EDITED)),
|
||||
cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
|
||||
cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)));
|
||||
}
|
||||
|
||||
public static Message createStatusMessage(Conversation conversation, String body) {
|
||||
|
@ -178,6 +194,14 @@ public class Message extends AbstractEntity {
|
|||
return message;
|
||||
}
|
||||
|
||||
public static Message createLoadMoreMessage(Conversation conversation) {
|
||||
final Message message = new Message();
|
||||
message.setType(Message.TYPE_STATUS);
|
||||
message.setConversation(conversation);
|
||||
message.setBody("LOAD_MORE");
|
||||
return message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentValues getContentValues() {
|
||||
ContentValues values = new ContentValues();
|
||||
|
@ -186,12 +210,12 @@ public class Message extends AbstractEntity {
|
|||
if (counterpart == null) {
|
||||
values.putNull(COUNTERPART);
|
||||
} else {
|
||||
values.put(COUNTERPART, counterpart.toString());
|
||||
values.put(COUNTERPART, counterpart.toPreppedString());
|
||||
}
|
||||
if (trueCounterpart == null) {
|
||||
values.putNull(TRUE_COUNTERPART);
|
||||
} else {
|
||||
values.put(TRUE_COUNTERPART, trueCounterpart.toString());
|
||||
values.put(TRUE_COUNTERPART, trueCounterpart.toPreppedString());
|
||||
}
|
||||
values.put(BODY, body);
|
||||
values.put(TIME_SENT, timeSent);
|
||||
|
@ -203,7 +227,10 @@ public class Message extends AbstractEntity {
|
|||
values.put(RELATIVE_FILE_PATH, relativeFilePath);
|
||||
values.put(SERVER_MSG_ID, serverMsgId);
|
||||
values.put(FINGERPRINT, axolotlFingerprint);
|
||||
values.put(READ,read);
|
||||
values.put(READ,read ? 1 : 0);
|
||||
values.put(EDITED, edited);
|
||||
values.put(OOB, oob ? 1 : 0);
|
||||
values.put(ERROR_MESSAGE,errorMessage);
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -245,9 +272,23 @@ public class Message extends AbstractEntity {
|
|||
}
|
||||
|
||||
public void setBody(String body) {
|
||||
if (body == null) {
|
||||
throw new Error("You should not set the message body to null");
|
||||
}
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
|
||||
public boolean setErrorMessage(String message) {
|
||||
boolean changed = (message != null && !message.equals(errorMessage))
|
||||
|| (message == null && errorMessage != null);
|
||||
this.errorMessage = message;
|
||||
return changed;
|
||||
}
|
||||
|
||||
public long getTimeSent() {
|
||||
return timeSent;
|
||||
}
|
||||
|
@ -332,10 +373,22 @@ public class Message extends AbstractEntity {
|
|||
this.carbon = carbon;
|
||||
}
|
||||
|
||||
public void setEdited(String edited) {
|
||||
this.edited = edited;
|
||||
}
|
||||
|
||||
public boolean edited() {
|
||||
return this.edited != null;
|
||||
}
|
||||
|
||||
public void setTrueCounterpart(Jid trueCounterpart) {
|
||||
this.trueCounterpart = trueCounterpart;
|
||||
}
|
||||
|
||||
public Jid getTrueCounterpart() {
|
||||
return this.trueCounterpart;
|
||||
}
|
||||
|
||||
public Transferable getTransferable() {
|
||||
return this.transferable;
|
||||
}
|
||||
|
@ -344,8 +397,8 @@ public class Message extends AbstractEntity {
|
|||
this.transferable = transferable;
|
||||
}
|
||||
|
||||
public boolean equals(Message message) {
|
||||
if (this.serverMsgId != null && message.getServerMsgId() != null) {
|
||||
public boolean similar(Message message) {
|
||||
if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
|
||||
return this.serverMsgId.equals(message.getServerMsgId());
|
||||
} else if (this.body == null || this.counterpart == null) {
|
||||
return false;
|
||||
|
@ -358,15 +411,18 @@ public class Message extends AbstractEntity {
|
|||
body = this.body;
|
||||
otherBody = message.body;
|
||||
}
|
||||
final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
|
||||
if (message.getRemoteMsgId() != null) {
|
||||
final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
|
||||
if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
|
||||
return true;
|
||||
}
|
||||
return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
|
||||
&& this.counterpart.equals(message.getCounterpart())
|
||||
&& (body.equals(otherBody)
|
||||
||(message.getEncryption() == Message.ENCRYPTION_PGP
|
||||
&& message.getRemoteMsgId().matches("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"))) ;
|
||||
&& matchingCounterpart
|
||||
&& (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
|
||||
} else {
|
||||
return this.remoteMsgId == null
|
||||
&& this.counterpart.equals(message.getCounterpart())
|
||||
&& matchingCounterpart
|
||||
&& body.equals(otherBody)
|
||||
&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
|
||||
}
|
||||
|
@ -401,19 +457,37 @@ public class Message extends AbstractEntity {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isLastCorrectableMessage() {
|
||||
Message next = next();
|
||||
while(next != null) {
|
||||
if (next.isCorrectable()) {
|
||||
return false;
|
||||
}
|
||||
next = next.next();
|
||||
}
|
||||
return isCorrectable();
|
||||
}
|
||||
|
||||
private boolean isCorrectable() {
|
||||
return getStatus() != STATUS_RECEIVED && !isCarbon();
|
||||
}
|
||||
|
||||
public boolean mergeable(final Message message) {
|
||||
return message != null &&
|
||||
(message.getType() == Message.TYPE_TEXT &&
|
||||
this.getTransferable() == null &&
|
||||
message.getTransferable() == null &&
|
||||
message.getEncryption() != Message.ENCRYPTION_PGP &&
|
||||
message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
|
||||
this.getType() == message.getType() &&
|
||||
//this.getStatus() == message.getStatus() &&
|
||||
isStatusMergeable(this.getStatus(), message.getStatus()) &&
|
||||
this.getEncryption() == message.getEncryption() &&
|
||||
this.getCounterpart() != null &&
|
||||
this.getCounterpart().equals(message.getCounterpart()) &&
|
||||
this.edited() == message.edited() &&
|
||||
(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
|
||||
this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
|
||||
!GeoHelper.isGeoUri(message.getBody()) &&
|
||||
!GeoHelper.isGeoUri(this.body) &&
|
||||
message.treatAsDownloadable() == Decision.NEVER &&
|
||||
|
@ -422,7 +496,7 @@ public class Message extends AbstractEntity {
|
|||
!this.getBody().startsWith(ME_COMMAND) &&
|
||||
!this.bodyIsHeart() &&
|
||||
!message.bodyIsHeart() &&
|
||||
this.isTrusted() == message.isTrusted()
|
||||
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint()))
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -437,19 +511,26 @@ public class Message extends AbstractEntity {
|
|||
);
|
||||
}
|
||||
|
||||
public String getMergedBody() {
|
||||
StringBuilder body = new StringBuilder(this.body.trim());
|
||||
public static class MergeSeparator {}
|
||||
|
||||
public SpannableStringBuilder getMergedBody() {
|
||||
SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
|
||||
Message current = this;
|
||||
while(current.mergeable(current.next())) {
|
||||
while (current.mergeable(current.next())) {
|
||||
current = current.next();
|
||||
body.append(MERGE_SEPARATOR);
|
||||
if (current == null) {
|
||||
break;
|
||||
}
|
||||
body.append("\n\n");
|
||||
body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
|
||||
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
body.append(current.getBody().trim());
|
||||
}
|
||||
return body.toString();
|
||||
return body;
|
||||
}
|
||||
|
||||
public boolean hasMeCommand() {
|
||||
return getMergedBody().startsWith(ME_COMMAND);
|
||||
return this.body.trim().startsWith(ME_COMMAND);
|
||||
}
|
||||
|
||||
public int getMergedStatus() {
|
||||
|
@ -457,6 +538,9 @@ public class Message extends AbstractEntity {
|
|||
Message current = this;
|
||||
while(current.mergeable(current.next())) {
|
||||
current = current.next();
|
||||
if (current == null) {
|
||||
break;
|
||||
}
|
||||
status = current.status;
|
||||
}
|
||||
return status;
|
||||
|
@ -467,6 +551,9 @@ public class Message extends AbstractEntity {
|
|||
Message current = this;
|
||||
while(current.mergeable(current.next())) {
|
||||
current = current.next();
|
||||
if (current == null) {
|
||||
break;
|
||||
}
|
||||
time = current.timeSent;
|
||||
}
|
||||
return time;
|
||||
|
@ -479,7 +566,7 @@ public class Message extends AbstractEntity {
|
|||
|
||||
public boolean trusted() {
|
||||
Contact contact = this.getContact();
|
||||
return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
|
||||
return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
|
||||
}
|
||||
|
||||
public boolean fixCounterpart() {
|
||||
|
@ -490,7 +577,7 @@ public class Message extends AbstractEntity {
|
|||
try {
|
||||
counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
|
||||
conversation.getJid().getDomainpart(),
|
||||
presences.asStringArray()[0]);
|
||||
presences.toResourceArray()[0]);
|
||||
return true;
|
||||
} catch (InvalidJidException e) {
|
||||
counterpart = null;
|
||||
|
@ -502,6 +589,18 @@ public class Message extends AbstractEntity {
|
|||
}
|
||||
}
|
||||
|
||||
public void setUuid(String uuid) {
|
||||
this.uuid = uuid;
|
||||
}
|
||||
|
||||
public String getEditedId() {
|
||||
return edited;
|
||||
}
|
||||
|
||||
public void setOob(boolean isOob) {
|
||||
this.oob = isOob;
|
||||
}
|
||||
|
||||
public enum Decision {
|
||||
MUST,
|
||||
SHOULD,
|
||||
|
@ -517,14 +616,14 @@ public class Message extends AbstractEntity {
|
|||
if (path == null || path.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
|
||||
int dotPosition = filename.lastIndexOf(".");
|
||||
|
||||
if (dotPosition != -1) {
|
||||
String extension = filename.substring(dotPosition + 1);
|
||||
// we want the real file extension, not the crypto one
|
||||
if (Arrays.asList(Transferable.VALID_CRYPTO_EXTENSIONS).contains(extension)) {
|
||||
if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
|
||||
return extractRelevantExtension(filename.substring(0,dotPosition));
|
||||
} else {
|
||||
return extension;
|
||||
|
@ -558,6 +657,8 @@ public class Message extends AbstractEntity {
|
|||
URL url = new URL(body);
|
||||
if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
|
||||
return Decision.NEVER;
|
||||
} else if (oob) {
|
||||
return Decision.MUST;
|
||||
}
|
||||
String extension = extractRelevantExtension(url);
|
||||
if (extension == null) {
|
||||
|
@ -567,13 +668,9 @@ public class Message extends AbstractEntity {
|
|||
boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
|
||||
|
||||
if (encrypted) {
|
||||
if (MimeUtils.guessMimeTypeFromExtension(extension) != null) {
|
||||
return Decision.MUST;
|
||||
} else {
|
||||
return Decision.NEVER;
|
||||
}
|
||||
} else if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension)
|
||||
|| Arrays.asList(Transferable.WELL_KNOWN_EXTENSIONS).contains(extension)) {
|
||||
return Decision.MUST;
|
||||
} else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension)
|
||||
|| Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) {
|
||||
return Decision.SHOULD;
|
||||
} else {
|
||||
return Decision.NEVER;
|
||||
|
@ -709,17 +806,17 @@ public class Message extends AbstractEntity {
|
|||
public int height = 0;
|
||||
}
|
||||
|
||||
public void setAxolotlFingerprint(String fingerprint) {
|
||||
public void setFingerprint(String fingerprint) {
|
||||
this.axolotlFingerprint = fingerprint;
|
||||
}
|
||||
|
||||
public String getAxolotlFingerprint() {
|
||||
public String getFingerprint() {
|
||||
return axolotlFingerprint;
|
||||
}
|
||||
|
||||
public boolean isTrusted() {
|
||||
XmppAxolotlSession.Trust t = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
|
||||
return t != null && t.trusted();
|
||||
FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
|
||||
return s != null && s.isTrusted();
|
||||
}
|
||||
|
||||
private int getPreviousEncryption() {
|
||||
|
|
|
@ -3,10 +3,9 @@ package eu.siacs.conversations.entities;
|
|||
import android.annotation.SuppressLint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.xmpp.forms.Data;
|
||||
|
@ -18,6 +17,8 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
|
|||
@SuppressLint("DefaultLocale")
|
||||
public class MucOptions {
|
||||
|
||||
private boolean mAutoPushConfiguration = true;
|
||||
|
||||
public Account getAccount() {
|
||||
return this.conversation.getAccount();
|
||||
}
|
||||
|
@ -26,6 +27,27 @@ public class MucOptions {
|
|||
this.self = user;
|
||||
}
|
||||
|
||||
public void changeAffiliation(Jid jid, Affiliation affiliation) {
|
||||
User user = findUserByRealJid(jid);
|
||||
synchronized (users) {
|
||||
if (user != null && user.getRole() == Role.NONE) {
|
||||
users.remove(user);
|
||||
if (affiliation.ranks(Affiliation.MEMBER)) {
|
||||
user.affiliation = affiliation;
|
||||
users.add(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void flagNoAutoPushConfiguration() {
|
||||
mAutoPushConfiguration = false;
|
||||
}
|
||||
|
||||
public boolean autoPushConfiguration() {
|
||||
return mAutoPushConfiguration;
|
||||
}
|
||||
|
||||
public enum Affiliation {
|
||||
OWNER("owner", 4, R.string.owner),
|
||||
ADMIN("admin", 3, R.string.admin),
|
||||
|
@ -91,22 +113,27 @@ public class MucOptions {
|
|||
}
|
||||
}
|
||||
|
||||
public static final int ERROR_NO_ERROR = 0;
|
||||
public static final int ERROR_NICK_IN_USE = 1;
|
||||
public static final int ERROR_UNKNOWN = 2;
|
||||
public static final int ERROR_PASSWORD_REQUIRED = 3;
|
||||
public static final int ERROR_BANNED = 4;
|
||||
public static final int ERROR_MEMBERS_ONLY = 5;
|
||||
public static final int ERROR_NO_RESPONSE = 6;
|
||||
public enum Error {
|
||||
NO_RESPONSE,
|
||||
SERVER_NOT_FOUND,
|
||||
NONE,
|
||||
NICK_IN_USE,
|
||||
PASSWORD_REQUIRED,
|
||||
BANNED,
|
||||
MEMBERS_ONLY,
|
||||
KICKED,
|
||||
SHUTDOWN,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public static final int KICKED_FROM_ROOM = 9;
|
||||
|
||||
public static final String STATUS_CODE_ROOM_CONFIG_CHANGED = "104";
|
||||
public static final String STATUS_CODE_SELF_PRESENCE = "110";
|
||||
public static final String STATUS_CODE_ROOM_CREATED = "201";
|
||||
public static final String STATUS_CODE_BANNED = "301";
|
||||
public static final String STATUS_CODE_CHANGED_NICK = "303";
|
||||
public static final String STATUS_CODE_KICKED = "307";
|
||||
public static final String STATUS_CODE_LOST_MEMBERSHIP = "321";
|
||||
public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
|
||||
public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
|
||||
public static final String STATUS_CODE_SHUTDOWN = "332";
|
||||
|
||||
private interface OnEventListener {
|
||||
void onSuccess();
|
||||
|
@ -118,10 +145,10 @@ public class MucOptions {
|
|||
|
||||
}
|
||||
|
||||
public static class User {
|
||||
public static class User implements Comparable<User> {
|
||||
private Role role = Role.NONE;
|
||||
private Affiliation affiliation = Affiliation.NONE;
|
||||
private Jid jid;
|
||||
private Jid realJid;
|
||||
private Jid fullJid;
|
||||
private long pgpKeyId = 0;
|
||||
private Avatar avatar;
|
||||
|
@ -133,15 +160,11 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public String getName() {
|
||||
return this.fullJid.getResourcepart();
|
||||
return fullJid == null ? null : fullJid.getResourcepart();
|
||||
}
|
||||
|
||||
public void setJid(Jid jid) {
|
||||
this.jid = jid;
|
||||
}
|
||||
|
||||
public Jid getJid() {
|
||||
return this.jid;
|
||||
public void setRealJid(Jid jid) {
|
||||
this.realJid = jid != null ? jid.toBareJid() : null;
|
||||
}
|
||||
|
||||
public Role getRole() {
|
||||
|
@ -149,6 +172,10 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public void setRole(String role) {
|
||||
if (role == null) {
|
||||
this.role = Role.NONE;
|
||||
return;
|
||||
}
|
||||
role = role.toLowerCase();
|
||||
switch (role) {
|
||||
case "moderator":
|
||||
|
@ -166,26 +193,15 @@ public class MucOptions {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
} else if (!(other instanceof User)) {
|
||||
return false;
|
||||
} else {
|
||||
User o = (User) other;
|
||||
return getName() != null && getName().equals(o.getName())
|
||||
&& jid != null && jid.equals(o.jid)
|
||||
&& affiliation == o.affiliation
|
||||
&& role == o.role;
|
||||
}
|
||||
}
|
||||
|
||||
public Affiliation getAffiliation() {
|
||||
return this.affiliation;
|
||||
}
|
||||
|
||||
public void setAffiliation(String affiliation) {
|
||||
if (affiliation == null) {
|
||||
this.affiliation = Affiliation.NONE;
|
||||
return;
|
||||
}
|
||||
affiliation = affiliation.toLowerCase();
|
||||
switch (affiliation) {
|
||||
case "admin":
|
||||
|
@ -214,7 +230,13 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public Contact getContact() {
|
||||
return getAccount().getRoster().getContactFromRoster(getJid());
|
||||
if (fullJid != null) {
|
||||
return getAccount().getRoster().getContactFromRoster(realJid);
|
||||
} else if (realJid != null){
|
||||
return getAccount().getRoster().getContact(realJid);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setAvatar(Avatar avatar) {
|
||||
|
@ -237,15 +259,74 @@ public class MucOptions {
|
|||
public Jid getFullJid() {
|
||||
return fullJid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
User user = (User) o;
|
||||
|
||||
if (role != user.role) return false;
|
||||
if (affiliation != user.affiliation) return false;
|
||||
if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
|
||||
return false;
|
||||
return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = role != null ? role.hashCode() : 0;
|
||||
result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
|
||||
result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
|
||||
result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "[fulljid:"+String.valueOf(fullJid)+",realjid:"+String.valueOf(realJid)+",affiliation"+affiliation.toString()+"]";
|
||||
}
|
||||
|
||||
public boolean realJidMatchesAccount() {
|
||||
return realJid != null && realJid.equals(options.account.getJid().toBareJid());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(User another) {
|
||||
if (another.getAffiliation().outranks(getAffiliation())) {
|
||||
return 1;
|
||||
} else if (getAffiliation().outranks(another.getAffiliation())) {
|
||||
return -1;
|
||||
} else {
|
||||
return getComparableName().compareToIgnoreCase(another.getComparableName());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private String getComparableName() {
|
||||
Contact contact = getContact();
|
||||
if (contact != null) {
|
||||
return contact.getDisplayName();
|
||||
} else {
|
||||
String name = getName();
|
||||
return name == null ? "" : name;
|
||||
}
|
||||
}
|
||||
|
||||
public Jid getRealJid() {
|
||||
return realJid;
|
||||
}
|
||||
}
|
||||
|
||||
private Account account;
|
||||
private final Map<String, User> users = Collections.synchronizedMap(new LinkedHashMap<String, User>());
|
||||
private List<String> features = new ArrayList<>();
|
||||
private final Set<User> users = new HashSet<>();
|
||||
private final List<String> features = new ArrayList<>();
|
||||
private Data form = new Data();
|
||||
private Conversation conversation;
|
||||
private boolean isOnline = false;
|
||||
private int error = ERROR_NO_RESPONSE;
|
||||
private Error error = Error.NONE;
|
||||
public OnRenameListener onRenameListener = null;
|
||||
private User self;
|
||||
private String subject = null;
|
||||
|
@ -282,7 +363,9 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public boolean participating() {
|
||||
return !online() || self.getRole().ranks(Role.PARTICIPANT);
|
||||
return !online()
|
||||
|| self.getRole().ranks(Role.PARTICIPANT)
|
||||
|| hasFeature("muc_unmoderated");
|
||||
}
|
||||
|
||||
public boolean membersOnly() {
|
||||
|
@ -306,24 +389,106 @@ public class MucOptions {
|
|||
return hasFeature("muc_moderated");
|
||||
}
|
||||
|
||||
public User deleteUser(String name) {
|
||||
return this.users.remove(name);
|
||||
public User deleteUser(Jid jid) {
|
||||
User user = findUserByFullJid(jid);
|
||||
if (user != null) {
|
||||
synchronized (users) {
|
||||
users.remove(user);
|
||||
boolean realJidInMuc = false;
|
||||
for (User u : users) {
|
||||
if (user.realJid != null && user.realJid.equals(u.realJid)) {
|
||||
realJidInMuc = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
boolean self = user.realJid != null && user.realJid.equals(account.getJid().toBareJid());
|
||||
if (membersOnly()
|
||||
&& nonanonymous()
|
||||
&& user.affiliation.ranks(Affiliation.MEMBER)
|
||||
&& user.realJid != null
|
||||
&& !realJidInMuc
|
||||
&& !self) {
|
||||
user.role = Role.NONE;
|
||||
user.avatar = null;
|
||||
user.fullJid = null;
|
||||
users.add(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
public void addUser(User user) {
|
||||
this.users.put(user.getName(), user);
|
||||
public void updateUser(User user) {
|
||||
User old;
|
||||
if (user.fullJid == null && user.realJid != null) {
|
||||
old = findUserByRealJid(user.realJid);
|
||||
if (old != null) {
|
||||
if (old.fullJid != null) {
|
||||
return; //don't add. user already exists
|
||||
} else {
|
||||
synchronized (users) {
|
||||
users.remove(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (user.realJid != null) {
|
||||
old = findUserByRealJid(user.realJid);
|
||||
synchronized (users) {
|
||||
if (old != null && old.fullJid == null) {
|
||||
users.remove(old);
|
||||
}
|
||||
}
|
||||
}
|
||||
old = findUserByFullJid(user.getFullJid());
|
||||
synchronized (this.users) {
|
||||
if (old != null) {
|
||||
users.remove(old);
|
||||
}
|
||||
if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
|
||||
&& user.getAffiliation().outranks(Affiliation.OUTCAST)){
|
||||
this.users.add(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public User findUser(String name) {
|
||||
return this.users.get(name);
|
||||
public User findUserByFullJid(Jid jid) {
|
||||
if (jid == null) {
|
||||
return null;
|
||||
}
|
||||
synchronized (users) {
|
||||
for (User user : users) {
|
||||
if (jid.equals(user.getFullJid())) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isUserInRoom(String name) {
|
||||
return findUser(name) != null;
|
||||
private User findUserByRealJid(Jid jid) {
|
||||
if (jid == null) {
|
||||
return null;
|
||||
}
|
||||
synchronized (users) {
|
||||
for (User user : users) {
|
||||
if (jid.equals(user.realJid)) {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setError(int error) {
|
||||
this.isOnline = isOnline && error == ERROR_NO_ERROR;
|
||||
public boolean isContactInRoom(Contact contact) {
|
||||
return findUserByRealJid(contact.getJid().toBareJid()) != null;
|
||||
}
|
||||
|
||||
public boolean isUserInRoom(Jid jid) {
|
||||
return findUserByFullJid(jid) != null;
|
||||
}
|
||||
|
||||
public void setError(Error error) {
|
||||
this.isOnline = isOnline && error == Error.NONE;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
|
@ -332,32 +497,53 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public ArrayList<User> getUsers() {
|
||||
return new ArrayList<>(users.values());
|
||||
return getUsers(true);
|
||||
}
|
||||
|
||||
public ArrayList<User> getUsers(boolean includeOffline) {
|
||||
synchronized (users) {
|
||||
if (includeOffline) {
|
||||
return new ArrayList<>(users);
|
||||
} else {
|
||||
ArrayList<User> onlineUsers = new ArrayList<>();
|
||||
for (User user : users) {
|
||||
if (user.getRole().ranks(Role.PARTICIPANT)) {
|
||||
onlineUsers.add(user);
|
||||
}
|
||||
}
|
||||
return onlineUsers;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<User> getUsers(int max) {
|
||||
ArrayList<User> users = new ArrayList<>();
|
||||
int i = 1;
|
||||
for(User user : this.users.values()) {
|
||||
users.add(user);
|
||||
if (i >= max) {
|
||||
break;
|
||||
} else {
|
||||
++i;
|
||||
ArrayList<User> subset = new ArrayList<>();
|
||||
HashSet<Jid> jids = new HashSet<>();
|
||||
jids.add(account.getJid().toBareJid());
|
||||
synchronized (users) {
|
||||
for(User user : users) {
|
||||
if (user.getRealJid() == null || jids.add(user.getRealJid())) {
|
||||
subset.add(user);
|
||||
}
|
||||
if (subset.size() >= max) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return users;
|
||||
return subset;
|
||||
}
|
||||
|
||||
public int getUserCount() {
|
||||
return this.users.size();
|
||||
synchronized (users) {
|
||||
return users.size();
|
||||
}
|
||||
}
|
||||
|
||||
public String getProposedNick() {
|
||||
if (conversation.getBookmark() != null
|
||||
&& conversation.getBookmark().getNick() != null
|
||||
&& !conversation.getBookmark().getNick().isEmpty()) {
|
||||
return conversation.getBookmark().getNick();
|
||||
&& !conversation.getBookmark().getNick().trim().isEmpty()) {
|
||||
return conversation.getBookmark().getNick().trim();
|
||||
} else if (!conversation.getJid().isBareJid()) {
|
||||
return conversation.getJid().getResourcepart();
|
||||
} else {
|
||||
|
@ -377,7 +563,7 @@ public class MucOptions {
|
|||
return this.isOnline;
|
||||
}
|
||||
|
||||
public int getError() {
|
||||
public Error getError() {
|
||||
return this.error;
|
||||
}
|
||||
|
||||
|
@ -386,8 +572,10 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public void setOffline() {
|
||||
this.users.clear();
|
||||
this.error = ERROR_NO_RESPONSE;
|
||||
synchronized (users) {
|
||||
this.users.clear();
|
||||
}
|
||||
this.error = Error.NO_RESPONSE;
|
||||
this.isOnline = false;
|
||||
}
|
||||
|
||||
|
@ -404,13 +592,13 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public String createNameFromParticipants() {
|
||||
if (users.size() >= 2) {
|
||||
if (getUserCount() >= 2) {
|
||||
List<String> names = new ArrayList<>();
|
||||
for (User user : getUsers(5)) {
|
||||
Contact contact = user.getContact();
|
||||
if (contact != null && !contact.getDisplayName().isEmpty()) {
|
||||
names.add(contact.getDisplayName().split("\\s+")[0]);
|
||||
} else {
|
||||
} else if (user.getName() != null){
|
||||
names.add(user.getName());
|
||||
}
|
||||
}
|
||||
|
@ -429,7 +617,7 @@ public class MucOptions {
|
|||
|
||||
public long[] getPgpKeyIds() {
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (User user : this.users.values()) {
|
||||
for (User user : this.users) {
|
||||
if (user.getPgpKeyId() != 0) {
|
||||
ids.add(user.getPgpKeyId());
|
||||
}
|
||||
|
@ -443,18 +631,22 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public boolean pgpKeysInUse() {
|
||||
for (User user : this.users.values()) {
|
||||
if (user.getPgpKeyId() != 0) {
|
||||
return true;
|
||||
synchronized (users) {
|
||||
for (User user : users) {
|
||||
if (user.getPgpKeyId() != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean everybodyHasKeys() {
|
||||
for (User user : this.users.values()) {
|
||||
if (user.getPgpKeyId() == 0) {
|
||||
return false;
|
||||
synchronized (users) {
|
||||
for (User user : users) {
|
||||
if (user.getPgpKeyId() == 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
@ -468,9 +660,12 @@ public class MucOptions {
|
|||
}
|
||||
}
|
||||
|
||||
public Jid getTrueCounterpart(String name) {
|
||||
User user = findUser(name);
|
||||
return user == null ? null : user.getJid();
|
||||
public Jid getTrueCounterpart(Jid jid) {
|
||||
if (jid.equals(getSelf().getFullJid())) {
|
||||
return account.getJid().toBareJid();
|
||||
}
|
||||
User user = findUserByFullJid(jid);
|
||||
return user == null ? null : user.realJid;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
|
@ -495,4 +690,16 @@ public class MucOptions {
|
|||
public Conversation getConversation() {
|
||||
return this.conversation;
|
||||
}
|
||||
|
||||
public List<Jid> getMembers() {
|
||||
ArrayList<Jid> members = new ArrayList<>();
|
||||
synchronized (users) {
|
||||
for (User user : users) {
|
||||
if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null) {
|
||||
members.add(user.realJid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
}
|
||||
|
|
93
src/main/java/eu/siacs/conversations/entities/Presence.java
Normal file
|
@ -0,0 +1,93 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import java.lang.Comparable;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
|
||||
public class Presence implements Comparable {
|
||||
|
||||
public enum Status {
|
||||
CHAT, ONLINE, AWAY, XA, DND, OFFLINE;
|
||||
|
||||
public String toShowString() {
|
||||
switch(this) {
|
||||
case CHAT: return "chat";
|
||||
case AWAY: return "away";
|
||||
case XA: return "xa";
|
||||
case DND: return "dnd";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Status fromShowString(String show) {
|
||||
if (show == null) {
|
||||
return ONLINE;
|
||||
} else {
|
||||
switch (show.toLowerCase(Locale.US)) {
|
||||
case "away":
|
||||
return AWAY;
|
||||
case "xa":
|
||||
return XA;
|
||||
case "dnd":
|
||||
return DND;
|
||||
case "chat":
|
||||
return CHAT;
|
||||
default:
|
||||
return ONLINE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final Status status;
|
||||
private ServiceDiscoveryResult disco;
|
||||
private final String ver;
|
||||
private final String hash;
|
||||
private final String message;
|
||||
|
||||
private Presence(Status status, String ver, String hash, String message) {
|
||||
this.status = status;
|
||||
this.ver = ver;
|
||||
this.hash = hash;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public static Presence parse(String show, Element caps, String message) {
|
||||
final String hash = caps == null ? null : caps.getAttribute("hash");
|
||||
final String ver = caps == null ? null : caps.getAttribute("ver");
|
||||
return new Presence(Status.fromShowString(show), ver, hash, message);
|
||||
}
|
||||
|
||||
public int compareTo(Object other) {
|
||||
return this.status.compareTo(((Presence)other).status);
|
||||
}
|
||||
|
||||
public Status getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
public boolean hasCaps() {
|
||||
return ver != null && hash != null;
|
||||
}
|
||||
|
||||
public String getVer() {
|
||||
return this.ver;
|
||||
}
|
||||
|
||||
public String getHash() {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) {
|
||||
this.disco = disco;
|
||||
}
|
||||
|
||||
public ServiceDiscoveryResult getServiceDiscoveryResult() {
|
||||
return disco;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
|
||||
|
||||
public class PresenceTemplate extends AbstractEntity {
|
||||
|
||||
public static final String TABELNAME = "presence_templates";
|
||||
public static final String LAST_USED = "last_used";
|
||||
public static final String MESSAGE = "message";
|
||||
public static final String STATUS = "status";
|
||||
|
||||
private long lastUsed = 0;
|
||||
private String statusMessage;
|
||||
private Presence.Status status = Presence.Status.ONLINE;
|
||||
|
||||
public PresenceTemplate(Presence.Status status, String statusMessage) {
|
||||
this.status = status;
|
||||
this.statusMessage = statusMessage;
|
||||
this.lastUsed = System.currentTimeMillis();
|
||||
this.uuid = java.util.UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
private PresenceTemplate() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ContentValues getContentValues() {
|
||||
final String show = status.toShowString();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(LAST_USED, lastUsed);
|
||||
values.put(MESSAGE, statusMessage);
|
||||
values.put(STATUS, show == null ? "" : show);
|
||||
values.put(UUID, uuid);
|
||||
return values;
|
||||
}
|
||||
|
||||
public static PresenceTemplate fromCursor(Cursor cursor) {
|
||||
PresenceTemplate template = new PresenceTemplate();
|
||||
template.uuid = cursor.getString(cursor.getColumnIndex(UUID));
|
||||
template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED));
|
||||
template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE));
|
||||
template.status = Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS)));
|
||||
return template;
|
||||
}
|
||||
|
||||
public Presence.Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getStatusMessage() {
|
||||
return statusMessage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
PresenceTemplate template = (PresenceTemplate) o;
|
||||
|
||||
if (statusMessage != null ? !statusMessage.equals(template.statusMessage) : template.statusMessage != null)
|
||||
return false;
|
||||
return status == template.status;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = statusMessage != null ? statusMessage.hashCode() : 0;
|
||||
result = 31 * result + status.hashCode();
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -1,29 +1,27 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
|
||||
public class Presences {
|
||||
private final Hashtable<String, Presence> presences = new Hashtable<>();
|
||||
|
||||
public static final int CHAT = -1;
|
||||
public static final int ONLINE = 0;
|
||||
public static final int AWAY = 1;
|
||||
public static final int XA = 2;
|
||||
public static final int DND = 3;
|
||||
public static final int OFFLINE = 4;
|
||||
|
||||
private final Hashtable<String, Integer> presences = new Hashtable<>();
|
||||
|
||||
public Hashtable<String, Integer> getPresences() {
|
||||
public Hashtable<String, Presence> getPresences() {
|
||||
return this.presences;
|
||||
}
|
||||
|
||||
public void updatePresence(String resource, int status) {
|
||||
public void updatePresence(String resource, Presence presence) {
|
||||
synchronized (this.presences) {
|
||||
this.presences.put(resource, status);
|
||||
this.presences.put(resource, presence);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -39,42 +37,27 @@ public class Presences {
|
|||
}
|
||||
}
|
||||
|
||||
public int getMostAvailableStatus() {
|
||||
int status = OFFLINE;
|
||||
public Presence.Status getShownStatus() {
|
||||
Presence.Status status = Presence.Status.OFFLINE;
|
||||
synchronized (this.presences) {
|
||||
Iterator<Entry<String, Integer>> it = presences.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Entry<String, Integer> entry = it.next();
|
||||
if (entry.getValue() < status)
|
||||
status = entry.getValue();
|
||||
for(Presence p : presences.values()) {
|
||||
if (p.getStatus() == Presence.Status.DND) {
|
||||
return p.getStatus();
|
||||
} else if (p.getStatus().compareTo(status) < 0){
|
||||
status = p.getStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
public static int parseShow(Element show) {
|
||||
if ((show == null) || (show.getContent() == null)) {
|
||||
return Presences.ONLINE;
|
||||
} else if (show.getContent().equals("away")) {
|
||||
return Presences.AWAY;
|
||||
} else if (show.getContent().equals("xa")) {
|
||||
return Presences.XA;
|
||||
} else if (show.getContent().equals("chat")) {
|
||||
return Presences.CHAT;
|
||||
} else if (show.getContent().equals("dnd")) {
|
||||
return Presences.DND;
|
||||
} else {
|
||||
return Presences.OFFLINE;
|
||||
}
|
||||
}
|
||||
|
||||
public int size() {
|
||||
synchronized (this.presences) {
|
||||
return presences.size();
|
||||
}
|
||||
}
|
||||
|
||||
public String[] asStringArray() {
|
||||
public String[] toResourceArray() {
|
||||
synchronized (this.presences) {
|
||||
final String[] presencesArray = new String[presences.size()];
|
||||
presences.keySet().toArray(presencesArray);
|
||||
|
@ -82,9 +65,70 @@ public class Presences {
|
|||
}
|
||||
}
|
||||
|
||||
public List<PresenceTemplate> asTemplates() {
|
||||
synchronized (this.presences) {
|
||||
ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
|
||||
for(Presence p : presences.values()) {
|
||||
if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
|
||||
templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
|
||||
}
|
||||
}
|
||||
return templates;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean has(String presence) {
|
||||
synchronized (this.presences) {
|
||||
return presences.containsKey(presence);
|
||||
}
|
||||
}
|
||||
|
||||
public List<String> getStatusMessages() {
|
||||
ArrayList<String> messages = new ArrayList<>();
|
||||
synchronized (this.presences) {
|
||||
for(Presence presence : this.presences.values()) {
|
||||
String message = presence.getMessage() == null ? null : presence.getMessage().trim();
|
||||
if (message != null && !message.isEmpty() && !messages.contains(message)) {
|
||||
messages.add(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
public boolean allOrNonSupport(String namespace) {
|
||||
synchronized (this.presences) {
|
||||
for(Presence presence : this.presences.values()) {
|
||||
ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
|
||||
if (disco == null || !disco.getFeatures().contains(namespace)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public Pair<Map<String, String>,Map<String,String>> toTypeAndNameMap() {
|
||||
Map<String,String> typeMap = new HashMap<>();
|
||||
Map<String,String> nameMap = new HashMap<>();
|
||||
synchronized (this.presences) {
|
||||
for(Map.Entry<String,Presence> presenceEntry : this.presences.entrySet()) {
|
||||
String resource = presenceEntry.getKey();
|
||||
Presence presence = presenceEntry.getValue();
|
||||
ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult();
|
||||
if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) {
|
||||
ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0);
|
||||
String type = identity.getType();
|
||||
String name = identity.getName();
|
||||
if (type != null) {
|
||||
typeMap.put(resource,type);
|
||||
}
|
||||
if (name != null) {
|
||||
nameMap.put(resource, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Pair(typeMap,nameMap);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import eu.siacs.conversations.xmpp.jid.Jid;
|
|||
|
||||
public class Roster {
|
||||
final Account account;
|
||||
final HashMap<String, Contact> contacts = new HashMap<>();
|
||||
final HashMap<Jid, Contact> contacts = new HashMap<>();
|
||||
private String version = null;
|
||||
|
||||
public Roster(Account account) {
|
||||
|
@ -21,7 +21,7 @@ public class Roster {
|
|||
return null;
|
||||
}
|
||||
synchronized (this.contacts) {
|
||||
Contact contact = contacts.get(jid.toBareJid().toString());
|
||||
Contact contact = contacts.get(jid.toBareJid());
|
||||
if (contact != null && contact.showInRoster()) {
|
||||
return contact;
|
||||
} else {
|
||||
|
@ -32,15 +32,13 @@ public class Roster {
|
|||
|
||||
public Contact getContact(final Jid jid) {
|
||||
synchronized (this.contacts) {
|
||||
final Jid bareJid = jid.toBareJid();
|
||||
if (contacts.containsKey(bareJid.toString())) {
|
||||
return contacts.get(bareJid.toString());
|
||||
} else {
|
||||
Contact contact = new Contact(bareJid);
|
||||
if (!contacts.containsKey(jid.toBareJid())) {
|
||||
Contact contact = new Contact(jid.toBareJid());
|
||||
contact.setAccount(account);
|
||||
contacts.put(bareJid.toString(), contact);
|
||||
contacts.put(contact.getJid().toBareJid(), contact);
|
||||
return contact;
|
||||
}
|
||||
return contacts.get(jid.toBareJid());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,7 +78,7 @@ public class Roster {
|
|||
contact.setAccount(account);
|
||||
contact.setOption(Contact.Options.IN_ROSTER);
|
||||
synchronized (this.contacts) {
|
||||
contacts.put(contact.getJid().toBareJid().toString(), contact);
|
||||
contacts.put(contact.getJid().toBareJid(), contact);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,348 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.util.Base64;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.Comparable;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.forms.Data;
|
||||
import eu.siacs.conversations.xmpp.forms.Field;
|
||||
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
|
||||
|
||||
public class ServiceDiscoveryResult {
|
||||
public static final String TABLENAME = "discovery_results";
|
||||
public static final String HASH = "hash";
|
||||
public static final String VER = "ver";
|
||||
public static final String RESULT = "result";
|
||||
|
||||
protected static String blankNull(String s) {
|
||||
return s == null ? "" : s;
|
||||
}
|
||||
|
||||
public static class Identity implements Comparable {
|
||||
protected final String category;
|
||||
protected final String type;
|
||||
protected final String lang;
|
||||
protected final String name;
|
||||
|
||||
public Identity(final String category, final String type, final String lang, final String name) {
|
||||
this.category = category;
|
||||
this.type = type;
|
||||
this.lang = lang;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Identity(final Element el) {
|
||||
this(
|
||||
el.getAttribute("category"),
|
||||
el.getAttribute("type"),
|
||||
el.getAttribute("xml:lang"),
|
||||
el.getAttribute("name")
|
||||
);
|
||||
}
|
||||
|
||||
public Identity(final JSONObject o) {
|
||||
|
||||
this(
|
||||
o.optString("category", null),
|
||||
o.optString("type", null),
|
||||
o.optString("lang", null),
|
||||
o.optString("name", null)
|
||||
);
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return this.category;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public String getLang() {
|
||||
return this.lang;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public int compareTo(Object other) {
|
||||
Identity o = (Identity)other;
|
||||
int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory()));
|
||||
if(r == 0) {
|
||||
r = blankNull(this.getType()).compareTo(blankNull(o.getType()));
|
||||
}
|
||||
if(r == 0) {
|
||||
r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
|
||||
}
|
||||
if(r == 0) {
|
||||
r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
public JSONObject toJSON() {
|
||||
try {
|
||||
JSONObject o = new JSONObject();
|
||||
o.put("category", this.getCategory());
|
||||
o.put("type", this.getType());
|
||||
o.put("lang", this.getLang());
|
||||
o.put("name", this.getName());
|
||||
return o;
|
||||
} catch(JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected final String hash;
|
||||
protected final byte[] ver;
|
||||
protected final List<Identity> identities;
|
||||
protected final List<String> features;
|
||||
protected final List<Data> forms;
|
||||
|
||||
public ServiceDiscoveryResult(final IqPacket packet) {
|
||||
this.identities = new ArrayList<>();
|
||||
this.features = new ArrayList<>();
|
||||
this.forms = new ArrayList<>();
|
||||
this.hash = "sha-1"; // We only support sha-1 for now
|
||||
|
||||
final List<Element> elements = packet.query().getChildren();
|
||||
|
||||
for (final Element element : elements) {
|
||||
if (element.getName().equals("identity")) {
|
||||
Identity id = new Identity(element);
|
||||
if (id.getType() != null && id.getCategory() != null) {
|
||||
identities.add(id);
|
||||
}
|
||||
} else if (element.getName().equals("feature")) {
|
||||
if (element.getAttribute("var") != null) {
|
||||
features.add(element.getAttribute("var"));
|
||||
}
|
||||
} else if (element.getName().equals("x") && "jabber:x:data".equals(element.getAttribute("xmlns"))) {
|
||||
forms.add(Data.parse(element));
|
||||
}
|
||||
}
|
||||
this.ver = this.mkCapHash();
|
||||
}
|
||||
|
||||
public ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
|
||||
this.identities = new ArrayList<>();
|
||||
this.features = new ArrayList<>();
|
||||
this.forms = new ArrayList<>();
|
||||
this.hash = hash;
|
||||
this.ver = ver;
|
||||
|
||||
JSONArray identities = o.optJSONArray("identities");
|
||||
if (identities != null) {
|
||||
for (int i = 0; i < identities.length(); i++) {
|
||||
this.identities.add(new Identity(identities.getJSONObject(i)));
|
||||
}
|
||||
}
|
||||
JSONArray features = o.optJSONArray("features");
|
||||
if (features != null) {
|
||||
for (int i = 0; i < features.length(); i++) {
|
||||
this.features.add(features.getString(i));
|
||||
}
|
||||
}
|
||||
JSONArray forms = o.optJSONArray("forms");
|
||||
if (forms != null) {
|
||||
for(int i = 0; i < forms.length(); i++) {
|
||||
this.forms.add(createFormFromJSONObject(forms.getJSONObject(i)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Data createFormFromJSONObject(JSONObject o) {
|
||||
Data data = new Data();
|
||||
JSONArray names = o.names();
|
||||
for(int i = 0; i < names.length(); ++i) {
|
||||
try {
|
||||
String name = names.getString(i);
|
||||
JSONArray jsonValues = o.getJSONArray(name);
|
||||
ArrayList<String> values = new ArrayList<>(jsonValues.length());
|
||||
for(int j = 0; j < jsonValues.length(); ++j) {
|
||||
values.add(jsonValues.getString(j));
|
||||
}
|
||||
data.put(name, values);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
private static JSONObject createJSONFromForm(Data data) {
|
||||
JSONObject object = new JSONObject();
|
||||
for(Field field : data.getFields()) {
|
||||
try {
|
||||
JSONArray jsonValues = new JSONArray();
|
||||
for(String value : field.getValues()) {
|
||||
jsonValues.put(value);
|
||||
}
|
||||
object.put(field.getFieldName(), jsonValues);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
try {
|
||||
JSONArray jsonValues = new JSONArray();
|
||||
jsonValues.put(data.getFormType());
|
||||
object.put(Data.FORM_TYPE, jsonValues);
|
||||
} catch(Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
public String getVer() {
|
||||
return new String(Base64.encode(this.ver, Base64.DEFAULT)).trim();
|
||||
}
|
||||
|
||||
public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
|
||||
this(
|
||||
cursor.getString(cursor.getColumnIndex(HASH)),
|
||||
Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT),
|
||||
new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT)))
|
||||
);
|
||||
}
|
||||
|
||||
public List<Identity> getIdentities() {
|
||||
return this.identities;
|
||||
}
|
||||
|
||||
public List<String> getFeatures() {
|
||||
return this.features;
|
||||
}
|
||||
|
||||
public boolean hasIdentity(String category, String type) {
|
||||
for(Identity id : this.getIdentities()) {
|
||||
if((category == null || id.getCategory().equals(category)) &&
|
||||
(type == null || id.getType().equals(type))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getExtendedDiscoInformation(String formType, String name) {
|
||||
for(Data form : this.forms) {
|
||||
if (formType.equals(form.getFormType())) {
|
||||
for(Field field: form.getFields()) {
|
||||
if (name.equals(field.getFieldName())) {
|
||||
return field.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected byte[] mkCapHash() {
|
||||
StringBuilder s = new StringBuilder();
|
||||
|
||||
List<Identity> identities = this.getIdentities();
|
||||
Collections.sort(identities);
|
||||
|
||||
for(Identity id : identities) {
|
||||
s.append(
|
||||
blankNull(id.getCategory()) + "/" +
|
||||
blankNull(id.getType()) + "/" +
|
||||
blankNull(id.getLang()) + "/" +
|
||||
blankNull(id.getName()) + "<"
|
||||
);
|
||||
}
|
||||
|
||||
List<String> features = this.getFeatures();
|
||||
Collections.sort(features);
|
||||
|
||||
for (String feature : features) {
|
||||
s.append(feature + "<");
|
||||
}
|
||||
|
||||
Collections.sort(forms, new Comparator<Data>() {
|
||||
@Override
|
||||
public int compare(Data lhs, Data rhs) {
|
||||
return lhs.getFormType().compareTo(rhs.getFormType());
|
||||
}
|
||||
});
|
||||
|
||||
for(Data form : forms) {
|
||||
s.append(form.getFormType() + "<");
|
||||
List<Field> fields = form.getFields();
|
||||
Collections.sort(fields, new Comparator<Field>() {
|
||||
@Override
|
||||
public int compare(Field lhs, Field rhs) {
|
||||
return lhs.getFieldName().compareTo(rhs.getFieldName());
|
||||
}
|
||||
});
|
||||
for(Field field : fields) {
|
||||
s.append(field.getFieldName()+"<");
|
||||
List<String> values = field.getValues();
|
||||
Collections.sort(values);
|
||||
for(String value : values) {
|
||||
s.append(value+"<");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-1");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return md.digest(s.toString().getBytes("UTF-8"));
|
||||
} catch(UnsupportedEncodingException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public JSONObject toJSON() {
|
||||
try {
|
||||
JSONObject o = new JSONObject();
|
||||
|
||||
JSONArray ids = new JSONArray();
|
||||
for(Identity id : this.getIdentities()) {
|
||||
ids.put(id.toJSON());
|
||||
}
|
||||
o.put("identities", ids);
|
||||
|
||||
o.put("features", new JSONArray(this.getFeatures()));
|
||||
|
||||
JSONArray forms = new JSONArray();
|
||||
for(Data data : this.forms) {
|
||||
forms.put(createJSONFromForm(data));
|
||||
}
|
||||
o.put("forms", forms);
|
||||
|
||||
return o;
|
||||
} catch(JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ContentValues getContentValues() {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(HASH, this.hash);
|
||||
values.put(VER, getVer());
|
||||
values.put(RESULT, this.toJSON().toString());
|
||||
return values;
|
||||
}
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
package eu.siacs.conversations.entities;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
public interface Transferable {
|
||||
|
||||
String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"};
|
||||
String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"};
|
||||
String[] WELL_KNOWN_EXTENSIONS = {"pdf","m4a","mp4"};
|
||||
List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
|
||||
List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg", "otr");
|
||||
List<String> WELL_KNOWN_EXTENSIONS = Arrays.asList("pdf","m4a","mp4","3gp","aac","amr","mp3");
|
||||
|
||||
int STATUS_UNKNOWN = 0x200;
|
||||
int STATUS_CHECKING = 0x201;
|
||||
|
|
|
@ -12,14 +12,18 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.PhoneHelper;
|
||||
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||
|
||||
public abstract class AbstractGenerator {
|
||||
private final String[] FEATURES = {
|
||||
"urn:xmpp:jingle:1",
|
||||
"urn:xmpp:jingle:apps:file-transfer:3",
|
||||
Content.Version.FT_3.getNamespace(),
|
||||
Content.Version.FT_4.getNamespace(),
|
||||
"urn:xmpp:jingle:transports:s5b:1",
|
||||
"urn:xmpp:jingle:transports:ibb:1",
|
||||
"http://jabber.org/protocol/muc",
|
||||
|
@ -30,17 +34,24 @@ public abstract class AbstractGenerator {
|
|||
"http://jabber.org/protocol/nick+notify",
|
||||
"urn:xmpp:ping",
|
||||
"jabber:iq:version",
|
||||
"http://jabber.org/protocol/chatstates",
|
||||
AxolotlService.PEP_DEVICE_LIST+"+notify"};
|
||||
"http://jabber.org/protocol/chatstates"
|
||||
};
|
||||
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
|
||||
"urn:xmpp:chat-markers:0",
|
||||
"urn:xmpp:receipts"
|
||||
};
|
||||
private final String[] MESSAGE_CORRECTION_FEATURES = {
|
||||
"urn:xmpp:message-correct:0"
|
||||
};
|
||||
private final String[] PRIVACY_SENSITIVE = {
|
||||
"urn:xmpp:time" //XEP-0202: Entity Time leaks time zone
|
||||
};
|
||||
private final String[] OTR = {
|
||||
"urn:xmpp:otr:0"
|
||||
};
|
||||
private String mVersion = null;
|
||||
protected final String IDENTITY_NAME = "Conversations";
|
||||
protected final String IDENTITY_TYPE = "phone";
|
||||
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
|
||||
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
|
||||
|
||||
protected XmppConnectionService mXmppConnectionService;
|
||||
|
||||
|
@ -56,12 +67,20 @@ public abstract class AbstractGenerator {
|
|||
}
|
||||
|
||||
public String getIdentityName() {
|
||||
return IDENTITY_NAME + " " + getIdentityVersion();
|
||||
return mXmppConnectionService.getString(R.string.app_name) + " " + getIdentityVersion();
|
||||
}
|
||||
|
||||
public String getIdentityType() {
|
||||
if ("chromium".equals(android.os.Build.BRAND)) {
|
||||
return "pc";
|
||||
} else {
|
||||
return mXmppConnectionService.getString(R.string.default_resource).toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
public String getCapHash() {
|
||||
StringBuilder s = new StringBuilder();
|
||||
s.append("client/" + IDENTITY_TYPE + "//" + getIdentityName() + "<");
|
||||
s.append("client/" + getIdentityType() + "//" + getIdentityName() + "<");
|
||||
MessageDigest md;
|
||||
try {
|
||||
md = MessageDigest.getInstance("SHA-1");
|
||||
|
@ -87,6 +106,18 @@ public abstract class AbstractGenerator {
|
|||
if (mXmppConnectionService.confirmMessages()) {
|
||||
features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
|
||||
}
|
||||
if (mXmppConnectionService.allowMessageCorrection()) {
|
||||
features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES));
|
||||
}
|
||||
if (Config.supportOmemo()) {
|
||||
features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
|
||||
}
|
||||
if (!mXmppConnectionService.useTorToConnect()) {
|
||||
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
|
||||
}
|
||||
if (Config.supportOtr()) {
|
||||
features.addAll(Arrays.asList(OTR));
|
||||
}
|
||||
Collections.sort(features);
|
||||
return features;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package eu.siacs.conversations.generator;
|
||||
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -9,13 +10,18 @@ import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
|||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
import java.util.UUID;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
|
@ -44,7 +50,7 @@ public class IqGenerator extends AbstractGenerator {
|
|||
query.setAttribute("node", request.query().getAttribute("node"));
|
||||
final Element identity = query.addChild("identity");
|
||||
identity.setAttribute("category", "client");
|
||||
identity.setAttribute("type", IDENTITY_TYPE);
|
||||
identity.setAttribute("type", getIdentityType());
|
||||
identity.setAttribute("name", getIdentityName());
|
||||
for (final String feature : getFeatures()) {
|
||||
query.addChild("feature").setAttribute("var", feature);
|
||||
|
@ -55,8 +61,26 @@ public class IqGenerator extends AbstractGenerator {
|
|||
public IqPacket versionResponse(final IqPacket request) {
|
||||
final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
|
||||
Element query = packet.query("jabber:iq:version");
|
||||
query.addChild("name").setContent(IDENTITY_NAME);
|
||||
query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name));
|
||||
query.addChild("version").setContent(getIdentityVersion());
|
||||
if ("chromium".equals(android.os.Build.BRAND)) {
|
||||
query.addChild("os").setContent("Chrome OS");
|
||||
} else{
|
||||
query.addChild("os").setContent("Android");
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket entityTimeResponse(IqPacket request) {
|
||||
final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
|
||||
Element time = packet.addChild("time","urn:xmpp:time");
|
||||
final long now = System.currentTimeMillis();
|
||||
time.addChild("utc").setContent(getTimestamp(now));
|
||||
TimeZone ourTimezone = TimeZone.getDefault();
|
||||
long offsetSeconds = ourTimezone.getOffset(now) / 1000;
|
||||
long offsetMinutes = offsetSeconds % (60 * 60);
|
||||
long offsetHours = offsetSeconds / (60 * 60);
|
||||
time.addChild("tzo").setContent(String.format("%02d",offsetHours)+":"+String.format("%02d",offsetMinutes));
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
@ -233,10 +257,14 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return iq;
|
||||
}
|
||||
|
||||
public IqPacket generateSetBlockRequest(final Jid jid) {
|
||||
public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) {
|
||||
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
|
||||
final Element block = iq.addChild("block", Xmlns.BLOCKING);
|
||||
block.addChild("item").setAttribute("jid", jid.toBareJid().toString());
|
||||
final Element item = block.addChild("item").setAttribute("jid", jid.toBareJid().toString());
|
||||
if (reportSpam) {
|
||||
item.addChild("report", "urn:xmpp:reporting:0").addChild("spam");
|
||||
}
|
||||
Log.d(Config.LOGTAG,iq.toString());
|
||||
return iq;
|
||||
}
|
||||
|
||||
|
@ -289,8 +317,8 @@ public class IqGenerator extends AbstractGenerator {
|
|||
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
|
||||
packet.setTo(host);
|
||||
Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD);
|
||||
request.addChild("filename").setContent(file.getName());
|
||||
Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD);
|
||||
request.addChild("filename").setContent(convertFilename(file.getName()));
|
||||
request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
|
||||
if (mime != null) {
|
||||
request.addChild("content-type").setContent(mime);
|
||||
|
@ -298,13 +326,75 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
private static String convertFilename(String name) {
|
||||
int pos = name.indexOf('.');
|
||||
if (pos != -1) {
|
||||
try {
|
||||
UUID uuid = UUID.fromString(name.substring(0, pos));
|
||||
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
|
||||
bb.putLong(uuid.getMostSignificantBits());
|
||||
bb.putLong(uuid.getLeastSignificantBits());
|
||||
return Base64.encodeToString(bb.array(), Base64.URL_SAFE) + name.substring(pos, name.length());
|
||||
} catch (Exception e) {
|
||||
return name;
|
||||
}
|
||||
} else {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
|
||||
public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
|
||||
final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
|
||||
|
||||
register.setFrom(account.getJid().toBareJid());
|
||||
register.setTo(account.getServer());
|
||||
register.setId(id);
|
||||
register.query("jabber:iq:register").addChild(data);
|
||||
|
||||
Element query = register.query("jabber:iq:register");
|
||||
if (data != null) {
|
||||
query.addChild(data);
|
||||
}
|
||||
return register;
|
||||
}
|
||||
|
||||
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
packet.setTo(appServer);
|
||||
Element command = packet.addChild("command", "http://jabber.org/protocol/commands");
|
||||
command.setAttribute("node","register-push-gcm");
|
||||
command.setAttribute("action","execute");
|
||||
Data data = new Data();
|
||||
data.put("token", token);
|
||||
data.put("device-id", deviceId);
|
||||
data.submit();
|
||||
command.addChild(data);
|
||||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket enablePush(Jid jid, String node, String secret) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
Element enable = packet.addChild("enable","urn:xmpp:push:0");
|
||||
enable.setAttribute("jid",jid.toString());
|
||||
enable.setAttribute("node", node);
|
||||
Data data = new Data();
|
||||
data.setFormType("http://jabber.org/protocol/pubsub#publish-options");
|
||||
data.put("secret",secret);
|
||||
data.submit();
|
||||
enable.addChild(data);
|
||||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
|
||||
packet.setTo(conversation.getJid().toBareJid());
|
||||
packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation",affiliation);
|
||||
return packet;
|
||||
}
|
||||
|
||||
public static Bundle defaultRoomConfiguration() {
|
||||
Bundle options = new Bundle();
|
||||
options.putString("muc#roomconfig_persistentroom", "1");
|
||||
options.putString("muc#roomconfig_membersonly", "1");
|
||||
options.putString("muc#roomconfig_publicroom", "0");
|
||||
options.putString("muc#roomconfig_whois", "anyone");
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,11 @@ import java.util.Date;
|
|||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
|
@ -20,6 +23,10 @@ import eu.siacs.conversations.xmpp.jid.Jid;
|
|||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||
|
||||
public class MessageGenerator extends AbstractGenerator {
|
||||
public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesn’t seem to support that";
|
||||
private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesn’t seem to support that. Find more information on https://conversations.im/omemo";
|
||||
private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesn’t seem to support that.";
|
||||
|
||||
public MessageGenerator(XmppConnectionService service) {
|
||||
super(service);
|
||||
}
|
||||
|
@ -47,6 +54,9 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
}
|
||||
packet.setFrom(account.getJid());
|
||||
packet.setId(message.getUuid());
|
||||
if (message.edited()) {
|
||||
packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId());
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
@ -65,10 +75,21 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
return null;
|
||||
}
|
||||
packet.setAxolotlMessage(axolotlMessage.toElement());
|
||||
if (Config.supportUnencrypted() && !recipientSupportsOmemo(message)) {
|
||||
packet.setBody(OMEMO_FALLBACK_MESSAGE);
|
||||
}
|
||||
packet.addChild("store", "urn:xmpp:hints");
|
||||
packet.addChild("encryption","urn:xmpp:eme:0")
|
||||
.setAttribute("name","OMEMO")
|
||||
.setAttribute("namespace",AxolotlService.PEP_PREFIX);
|
||||
return packet;
|
||||
}
|
||||
|
||||
private static boolean recipientSupportsOmemo(Message message) {
|
||||
Contact c = message.getContact();
|
||||
return c != null && c.getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
|
||||
}
|
||||
|
||||
public static void addMessageHints(MessagePacket packet) {
|
||||
packet.addChild("private", "urn:xmpp:carbons:2");
|
||||
packet.addChild("no-copy", "urn:xmpp:hints");
|
||||
|
@ -91,6 +112,8 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
content = message.getBody();
|
||||
}
|
||||
packet.setBody(otrSession.transformSending(content)[0]);
|
||||
packet.addChild("encryption","urn:xmpp:eme:0")
|
||||
.setAttribute("namespace","urn:xmpp:otr:0");
|
||||
return packet;
|
||||
} catch (OtrException e) {
|
||||
return null;
|
||||
|
@ -113,12 +136,16 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
|
||||
public MessagePacket generatePgpChat(Message message) {
|
||||
MessagePacket packet = preparePacket(message);
|
||||
packet.setBody("This is an XEP-0027 encrypted message");
|
||||
if (Config.supportUnencrypted()) {
|
||||
packet.setBody(PGP_FALLBACK_MESSAGE);
|
||||
}
|
||||
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
||||
packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
|
||||
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||
packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
|
||||
}
|
||||
packet.addChild("encryption","urn:xmpp:eme:0")
|
||||
.setAttribute("namespace","jabber:x:encrypted");
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
@ -163,6 +190,10 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
packet.setFrom(conversation.getAccount().getJid());
|
||||
Element x = packet.addChild("x", "jabber:x:conference");
|
||||
x.setAttribute("jid", conversation.getJid().toBareJid().toString());
|
||||
String password = conversation.getMucOptions().getPassword();
|
||||
if (password != null) {
|
||||
x.setAttribute("password",password);
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package eu.siacs.conversations.generator;
|
|||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Presences;
|
||||
import eu.siacs.conversations.entities.Presence;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
|
||||
|
@ -37,25 +37,18 @@ public class PresenceGenerator extends AbstractGenerator {
|
|||
return subscription("subscribed", contact);
|
||||
}
|
||||
|
||||
public PresencePacket selfPresence(Account account, int presence) {
|
||||
public PresencePacket selfPresence(Account account, Presence.Status status) {
|
||||
return selfPresence(account, status, true);
|
||||
}
|
||||
|
||||
public PresencePacket selfPresence(Account account, Presence.Status status, boolean includePgpAnnouncement) {
|
||||
PresencePacket packet = new PresencePacket();
|
||||
switch(presence) {
|
||||
case Presences.AWAY:
|
||||
packet.addChild("show").setContent("away");
|
||||
break;
|
||||
case Presences.XA:
|
||||
packet.addChild("show").setContent("xa");
|
||||
break;
|
||||
case Presences.CHAT:
|
||||
packet.addChild("show").setContent("chat");
|
||||
break;
|
||||
case Presences.DND:
|
||||
packet.addChild("show").setContent("dnd");
|
||||
break;
|
||||
if(status.toShowString() != null) {
|
||||
packet.addChild("show").setContent(status.toShowString());
|
||||
}
|
||||
packet.setFrom(account.getJid());
|
||||
String sig = account.getPgpSignature();
|
||||
if (sig != null) {
|
||||
if (includePgpAnnouncement && sig != null && mXmppConnectionService.getPgpEngine() != null) {
|
||||
packet.addChild("x", "jabber:x:signed").setContent(sig);
|
||||
}
|
||||
String capHash = getCapHash();
|
||||
|
|
|
@ -23,6 +23,8 @@ import eu.siacs.conversations.entities.Message;
|
|||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.SSLSocketHelper;
|
||||
import eu.siacs.conversations.utils.TLSSocketFactory;
|
||||
|
||||
public class HttpConnectionManager extends AbstractConnectionManager {
|
||||
|
||||
|
@ -63,7 +65,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
final X509TrustManager trustManager;
|
||||
final HostnameVerifier hostnameVerifier;
|
||||
if (interactive) {
|
||||
trustManager = mXmppConnectionService.getMemorizingTrustManager();
|
||||
trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
|
||||
hostnameVerifier = mXmppConnectionService
|
||||
.getMemorizingTrustManager().wrapHostnameVerifier(
|
||||
new StrictHostnameVerifier());
|
||||
|
@ -76,18 +78,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
new StrictHostnameVerifier());
|
||||
}
|
||||
try {
|
||||
final SSLContext sc = SSLContext.getInstance("TLS");
|
||||
sc.init(null, new X509TrustManager[]{trustManager},
|
||||
mXmppConnectionService.getRNG());
|
||||
|
||||
final SSLSocketFactory sf = sc.getSocketFactory();
|
||||
final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
|
||||
sf.getSupportedCipherSuites());
|
||||
if (cipherSuites.length > 0) {
|
||||
sc.getDefaultSSLParameters().setCipherSuites(cipherSuites);
|
||||
|
||||
}
|
||||
|
||||
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
||||
connection.setSSLSocketFactory(sf);
|
||||
connection.setHostnameVerifier(hostnameVerifier);
|
||||
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
|
||||
|
@ -95,6 +86,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
}
|
||||
|
||||
public Proxy getProxy() throws IOException {
|
||||
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getLocalHost(), 8118));
|
||||
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127,0,0,1}), 8118));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -10,12 +8,8 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
@ -31,6 +25,7 @@ import eu.siacs.conversations.persistance.FileBackend;
|
|||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.FileWriterException;
|
||||
|
||||
public class HttpDownloadConnection implements Transferable {
|
||||
|
||||
|
@ -89,12 +84,12 @@ public class HttpDownloadConnection implements Transferable {
|
|||
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
||||
}
|
||||
String extension;
|
||||
if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) {
|
||||
if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
|
||||
extension = secondToLast;
|
||||
} else {
|
||||
extension = lastPart;
|
||||
}
|
||||
message.setRelativeFilePath(message.getUuid()+"."+extension);
|
||||
message.setRelativeFilePath(message.getUuid() + "." + extension);
|
||||
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
|
||||
String reference = mUrl.getRef();
|
||||
if (reference != null && reference.length() == 96) {
|
||||
|
@ -125,35 +120,35 @@ public class HttpDownloadConnection implements Transferable {
|
|||
} else {
|
||||
message.setTransferable(null);
|
||||
}
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
mHttpConnectionManager.updateConversationUi(true);
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||
intent.setData(Uri.fromFile(file));
|
||||
mXmppConnectionService.sendBroadcast(intent);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
message.setTransferable(null);
|
||||
mHttpConnectionManager.finishConnection(this);
|
||||
boolean notify = acceptedAutomatically && !message.isRead();
|
||||
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||
message.getConversation().getAccount().getPgpDecryptionService().add(message);
|
||||
notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
|
||||
}
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
if (acceptedAutomatically) {
|
||||
mHttpConnectionManager.updateConversationUi(true);
|
||||
if (notify) {
|
||||
mXmppConnectionService.getNotificationService().push(message);
|
||||
}
|
||||
}
|
||||
|
||||
private void changeStatus(int status) {
|
||||
this.mStatus = status;
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
mHttpConnectionManager.updateConversationUi(true);
|
||||
}
|
||||
|
||||
private void showToastForException(Exception e) {
|
||||
e.printStackTrace();
|
||||
if (e instanceof java.net.UnknownHostException) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
|
||||
} else if (e instanceof java.net.ConnectException) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
|
||||
} else if (e instanceof FileWriterException) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
|
||||
} else if (!(e instanceof CancellationException)) {
|
||||
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
|
||||
}
|
||||
|
@ -172,21 +167,22 @@ public class HttpDownloadConnection implements Transferable {
|
|||
long size;
|
||||
try {
|
||||
size = retrieveFileSize();
|
||||
} catch (SSLHandshakeException e) {
|
||||
} catch (Exception e) {
|
||||
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
|
||||
HttpDownloadConnection.this.acceptedAutomatically = false;
|
||||
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
|
||||
return;
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
|
||||
if (interactive) {
|
||||
showToastForException(e);
|
||||
} else {
|
||||
HttpDownloadConnection.this.acceptedAutomatically = false;
|
||||
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
|
||||
}
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
file.setExpectedSize(size);
|
||||
if (mHttpConnectionManager.hasStoragePermission() && size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
|
||||
if (mHttpConnectionManager.hasStoragePermission()
|
||||
&& size <= mHttpConnectionManager.getAutoAcceptFileSize()
|
||||
&& mXmppConnectionService.isDataSaverDisabled()) {
|
||||
HttpDownloadConnection.this.acceptedAutomatically = true;
|
||||
new Thread(new FileDownloader(interactive)).start();
|
||||
} else {
|
||||
|
@ -213,11 +209,13 @@ public class HttpDownloadConnection implements Transferable {
|
|||
if (connection instanceof HttpsURLConnection) {
|
||||
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
|
||||
}
|
||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.connect();
|
||||
String contentLength = connection.getHeaderField("Content-Length");
|
||||
connection.disconnect();
|
||||
if (contentLength == null) {
|
||||
throw new IOException();
|
||||
throw new IOException("no content-length found in HEAD response");
|
||||
}
|
||||
return Long.parseLong(contentLength, 10);
|
||||
} catch (IOException e) {
|
||||
|
@ -251,6 +249,9 @@ public class HttpDownloadConnection implements Transferable {
|
|||
} catch (Exception e) {
|
||||
if (interactive) {
|
||||
showToastForException(e);
|
||||
} else {
|
||||
HttpDownloadConnection.this.acceptedAutomatically = false;
|
||||
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
|
@ -272,14 +273,18 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
|
||||
final boolean tryResume = file.exists() && file.getKey() == null;
|
||||
long resumeSize = 0;
|
||||
if (tryResume) {
|
||||
Log.d(Config.LOGTAG,"http download trying resume");
|
||||
long size = file.getSize();
|
||||
connection.setRequestProperty("Range", "bytes="+size+"-");
|
||||
resumeSize = file.getSize();
|
||||
connection.setRequestProperty("Range", "bytes="+resumeSize+"-");
|
||||
}
|
||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.connect();
|
||||
is = new BufferedInputStream(connection.getInputStream());
|
||||
boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges"));
|
||||
final String contentRange = connection.getHeaderField("Content-Range");
|
||||
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes "+resumeSize+"-");
|
||||
long transmitted = 0;
|
||||
long expected = file.getExpectedSize();
|
||||
if (tryResume && serverResumed) {
|
||||
|
@ -287,31 +292,38 @@ public class HttpDownloadConnection implements Transferable {
|
|||
transmitted = file.getSize();
|
||||
updateProgress((int) ((((double) transmitted) / expected) * 100));
|
||||
os = AbstractConnectionManager.createAppendedOutputStream(file);
|
||||
if (os == null) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
} else {
|
||||
file.getParentFile().mkdirs();
|
||||
file.createNewFile();
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
os = AbstractConnectionManager.createOutputStream(file, true);
|
||||
}
|
||||
int count = -1;
|
||||
int count;
|
||||
byte[] buffer = new byte[1024];
|
||||
while ((count = is.read(buffer)) != -1) {
|
||||
transmitted += count;
|
||||
os.write(buffer, 0, count);
|
||||
try {
|
||||
os.write(buffer, 0, count);
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
updateProgress((int) ((((double) transmitted) / expected) * 100));
|
||||
if (canceled) {
|
||||
throw new CancellationException();
|
||||
}
|
||||
}
|
||||
try {
|
||||
os.flush();
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
} catch (CancellationException | IOException e) {
|
||||
throw e;
|
||||
} finally {
|
||||
if (os != null) {
|
||||
try {
|
||||
os.flush();
|
||||
} catch (final IOException ignored) {
|
||||
|
||||
}
|
||||
}
|
||||
FileBackend.close(os);
|
||||
FileBackend.close(is);
|
||||
wakeLock.release();
|
||||
|
@ -328,7 +340,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
public void updateProgress(int i) {
|
||||
this.mProgress = i;
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
mHttpConnectionManager.updateConversationUi(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package eu.siacs.conversations.http;
|
||||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.PowerManager;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
@ -12,10 +10,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
@ -25,6 +20,7 @@ import eu.siacs.conversations.entities.Account;
|
|||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.parser.IqParser;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.AbstractConnectionManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
|
@ -91,10 +87,10 @@ public class HttpUploadConnection implements Transferable {
|
|||
this.canceled = true;
|
||||
}
|
||||
|
||||
private void fail() {
|
||||
private void fail(String errorMessage) {
|
||||
mHttpConnectionManager.finishUploadConnection(this);
|
||||
message.setTransferable(null);
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
|
||||
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, errorMessage);
|
||||
FileBackend.close(mFileInputStream);
|
||||
}
|
||||
|
||||
|
@ -115,7 +111,8 @@ public class HttpUploadConnection implements Transferable {
|
|||
try {
|
||||
pair = AbstractConnectionManager.createInputStream(file, true);
|
||||
} catch (FileNotFoundException e) {
|
||||
fail();
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not find file to upload - "+e.getMessage());
|
||||
fail(e.getMessage());
|
||||
return;
|
||||
}
|
||||
this.file.setExpectedSize(pair.second);
|
||||
|
@ -134,15 +131,14 @@ public class HttpUploadConnection implements Transferable {
|
|||
if (!canceled) {
|
||||
new Thread(new FileUploader()).start();
|
||||
}
|
||||
return;
|
||||
} catch (MalformedURLException e) {
|
||||
fail();
|
||||
//fall through
|
||||
}
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
} else {
|
||||
fail();
|
||||
}
|
||||
Log.d(Config.LOGTAG,account.getJid().toString()+": invalid response to slot request "+packet);
|
||||
fail(IqParser.extractErrorMessage(packet));
|
||||
}
|
||||
});
|
||||
message.setTransferable(this);
|
||||
|
@ -176,15 +172,17 @@ public class HttpUploadConnection implements Transferable {
|
|||
connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime);
|
||||
connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
|
||||
connection.setDoOutput(true);
|
||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.connect();
|
||||
os = connection.getOutputStream();
|
||||
transmitted = 0;
|
||||
int count = -1;
|
||||
int count;
|
||||
byte[] buffer = new byte[4096];
|
||||
while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
|
||||
transmitted += count;
|
||||
os.write(buffer, 0, count);
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
mHttpConnectionManager.updateConversationUi(false);
|
||||
}
|
||||
os.flush();
|
||||
os.close();
|
||||
|
@ -196,9 +194,7 @@ public class HttpUploadConnection implements Transferable {
|
|||
mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key));
|
||||
}
|
||||
mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl);
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||
intent.setData(Uri.fromFile(file));
|
||||
mXmppConnectionService.sendBroadcast(intent);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
|
||||
message.setTransferable(null);
|
||||
message.setCounterpart(message.getConversation().getJid().toBareJid());
|
||||
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
|
||||
|
@ -210,24 +206,26 @@ public class HttpUploadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public void error(int errorCode, Message object) {
|
||||
fail();
|
||||
Log.d(Config.LOGTAG,"pgp encryption failed");
|
||||
fail("pgp encryption failed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userInputRequried(PendingIntent pi, Message object) {
|
||||
fail();
|
||||
fail("pgp encryption failed");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mXmppConnectionService.resendMessage(message, delayed);
|
||||
}
|
||||
} else {
|
||||
fail();
|
||||
Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
|
||||
fail("http upload failed because response code was "+code);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
|
||||
fail();
|
||||
fail(e.getMessage());
|
||||
} finally {
|
||||
FileBackend.close(mFileInputStream);
|
||||
FileBackend.close(os);
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
package eu.siacs.conversations.parser;
|
||||
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.MucOptions;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
|
||||
|
||||
public abstract class AbstractParser {
|
||||
|
||||
|
@ -21,46 +21,48 @@ public abstract class AbstractParser {
|
|||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
public static Long getTimestamp(Element element, Long defaultValue) {
|
||||
public static Long parseTimestamp(Element element, Long d) {
|
||||
Element delay = element.findChild("delay","urn:xmpp:delay");
|
||||
if (delay != null) {
|
||||
String stamp = delay.getAttribute("stamp");
|
||||
if (stamp != null) {
|
||||
try {
|
||||
return AbstractParser.parseTimestamp(delay.getAttribute("stamp")).getTime();
|
||||
return AbstractParser.parseTimestamp(delay.getAttribute("stamp"));
|
||||
} catch (ParseException e) {
|
||||
return defaultValue;
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
return d;
|
||||
}
|
||||
|
||||
protected long getTimestamp(Element packet) {
|
||||
return getTimestamp(packet,System.currentTimeMillis());
|
||||
public static long parseTimestamp(Element element) {
|
||||
return parseTimestamp(element, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public static Date parseTimestamp(String timestamp) throws ParseException {
|
||||
public static long parseTimestamp(String timestamp) throws ParseException {
|
||||
timestamp = timestamp.replace("Z", "+0000");
|
||||
SimpleDateFormat dateFormat;
|
||||
long ms;
|
||||
if (timestamp.charAt(19) == '.' && timestamp.length() >= 25) {
|
||||
String millis = timestamp.substring(19,timestamp.length() - 5);
|
||||
try {
|
||||
double fractions = Double.parseDouble("0" + millis);
|
||||
ms = Math.round(1000 * fractions);
|
||||
} catch (NumberFormatException e) {
|
||||
ms = 0;
|
||||
}
|
||||
} else {
|
||||
ms = 0;
|
||||
}
|
||||
timestamp = timestamp.substring(0,19)+timestamp.substring(timestamp.length() -5,timestamp.length());
|
||||
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US);
|
||||
return dateFormat.parse(timestamp);
|
||||
return Math.min(dateFormat.parse(timestamp).getTime()+ms, System.currentTimeMillis());
|
||||
}
|
||||
|
||||
protected void updateLastseen(final AbstractStanza packet, final Account account, final boolean presenceOverwrite) {
|
||||
updateLastseen(getTimestamp(packet), account, packet.getFrom(), presenceOverwrite);
|
||||
}
|
||||
|
||||
protected void updateLastseen(long timestamp, final Account account, final Jid from, final boolean presenceOverwrite) {
|
||||
final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart();
|
||||
protected void updateLastseen(final Account account, final Jid from) {
|
||||
final Contact contact = account.getRoster().getContact(from);
|
||||
if (timestamp >= contact.lastseen.time) {
|
||||
contact.lastseen.time = timestamp;
|
||||
if (!presence.isEmpty() && presenceOverwrite) {
|
||||
contact.lastseen.presence = presence;
|
||||
}
|
||||
}
|
||||
contact.setLastResource(from.isBareJid() ? "" : from.getResourcepart());
|
||||
}
|
||||
|
||||
protected String avatarData(Element items) {
|
||||
|
@ -70,4 +72,43 @@ public abstract class AbstractParser {
|
|||
}
|
||||
return item.findChildContent("data", "urn:xmpp:avatar:data");
|
||||
}
|
||||
|
||||
public static MucOptions.User parseItem(Conversation conference, Element item) {
|
||||
return parseItem(conference,item, null);
|
||||
}
|
||||
|
||||
public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid) {
|
||||
final String local = conference.getJid().getLocalpart();
|
||||
final String domain = conference.getJid().getDomainpart();
|
||||
String affiliation = item.getAttribute("affiliation");
|
||||
String role = item.getAttribute("role");
|
||||
String nick = item.getAttribute("nick");
|
||||
if (nick != null && fullJid == null) {
|
||||
try {
|
||||
fullJid = Jid.fromParts(local, domain, nick);
|
||||
} catch (InvalidJidException e) {
|
||||
fullJid = null;
|
||||
}
|
||||
}
|
||||
Jid realJid = item.getAttributeAsJid("jid");
|
||||
MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid);
|
||||
user.setRealJid(realJid);
|
||||
user.setAffiliation(affiliation);
|
||||
user.setRole(role);
|
||||
return user;
|
||||
}
|
||||
|
||||
public static String extractErrorMessage(Element packet) {
|
||||
final Element error = packet.findChild("error");
|
||||
if (error != null && error.getChildren().size() > 0) {
|
||||
final String text = error.findChildContent("text");
|
||||
if (text != null && !text.trim().isEmpty()) {
|
||||
return text;
|
||||
} else {
|
||||
return error.getChildren().get(0).getName().replace("-"," ");
|
||||
}
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.util.Log;
|
|||
import android.util.Pair;
|
||||
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyBundle;
|
||||
|
@ -27,6 +26,7 @@ import eu.siacs.conversations.Config;
|
|||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.Xmlns;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
|
@ -55,6 +55,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
final String name = item.getAttribute("name");
|
||||
final String subscription = item.getAttribute("subscription");
|
||||
final Contact contact = account.getRoster().getContact(jid);
|
||||
boolean bothPre = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
|
||||
if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
|
||||
contact.setServerName(name);
|
||||
contact.parseGroupsFromElement(item);
|
||||
|
@ -70,6 +71,14 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
contact.parseSubscriptionFromElement(item);
|
||||
}
|
||||
}
|
||||
boolean both = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
|
||||
if ((both != bothPre) && both) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": gained mutual presence subscription with "+contact.getJid());
|
||||
AxolotlService axolotlService = account.getAxolotlService();
|
||||
if (axolotlService != null) {
|
||||
axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
|
||||
}
|
||||
}
|
||||
mXmppConnectionService.getAvatarService().clear(contact);
|
||||
}
|
||||
}
|
||||
|
@ -131,7 +140,11 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
if(signedPreKeyPublic == null) {
|
||||
return null;
|
||||
}
|
||||
return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
|
||||
try {
|
||||
return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ECPublicKey signedPreKeyPublic(final Element bundle) {
|
||||
|
@ -142,7 +155,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
}
|
||||
try {
|
||||
publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
|
||||
} catch (InvalidKeyException | IllegalArgumentException e) {
|
||||
} catch (Throwable e) {
|
||||
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage());
|
||||
}
|
||||
return publicKey;
|
||||
|
@ -155,7 +168,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
}
|
||||
try {
|
||||
return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT);
|
||||
} catch (IllegalArgumentException e) {
|
||||
} catch (Throwable e) {
|
||||
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : Invalid base64 in signedPreKeySignature");
|
||||
return null;
|
||||
}
|
||||
|
@ -169,7 +182,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
}
|
||||
try {
|
||||
identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
|
||||
} catch (InvalidKeyException | IllegalArgumentException e) {
|
||||
} catch (Throwable e) {
|
||||
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
|
||||
}
|
||||
return identityKey;
|
||||
|
@ -196,13 +209,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
|
||||
continue;
|
||||
}
|
||||
Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
|
||||
Integer preKeyId = null;
|
||||
try {
|
||||
ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
|
||||
preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
|
||||
final ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
|
||||
preKeyRecords.put(preKeyId, preKeyPublic);
|
||||
} catch (InvalidKeyException | IllegalArgumentException e) {
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"could not parse preKeyId from preKey "+preKeyPublicElement.toString());
|
||||
} catch (Throwable e) {
|
||||
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return preKeyRecords;
|
||||
|
@ -245,7 +260,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
Integer signedPreKeyId = signedPreKeyId(bundleElement);
|
||||
byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
|
||||
IdentityKey identityKey = identityKey(bundleElement);
|
||||
if(signedPreKeyPublic == null || identityKey == null) {
|
||||
if(signedPreKeyId == null || signedPreKeyPublic == null || identityKey == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -269,6 +284,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
|
||||
@Override
|
||||
public void onIqPacketReceived(final Account account, final IqPacket packet) {
|
||||
final boolean isGet = packet.getType() == IqPacket.TYPE.GET;
|
||||
if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
|
||||
return;
|
||||
} else if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
|
||||
|
@ -304,9 +320,21 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
}
|
||||
}
|
||||
account.getBlocklist().addAll(jids);
|
||||
if (packet.getType() == IqPacket.TYPE.SET) {
|
||||
for(Jid jid : jids) {
|
||||
Conversation conversation = mXmppConnectionService.find(account,jid);
|
||||
if (conversation != null) {
|
||||
mXmppConnectionService.markRead(conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update the UI
|
||||
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
|
||||
if (packet.getType() == IqPacket.TYPE.SET) {
|
||||
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
|
||||
mXmppConnectionService.sendIqPacket(account, response, null);
|
||||
}
|
||||
} else if (packet.hasChild("unblock", Xmlns.BLOCKING) &&
|
||||
packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) {
|
||||
Log.d(Config.LOGTAG, "Received unblock update from server");
|
||||
|
@ -327,19 +355,33 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
account.getBlocklist().removeAll(jids);
|
||||
}
|
||||
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
|
||||
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
|
||||
mXmppConnectionService.sendIqPacket(account, response, null);
|
||||
} else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
|
||||
|| packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
|
||||
|| packet.hasChild("data", "http://jabber.org/protocol/ibb")
|
||||
|| packet.hasChild("close","http://jabber.org/protocol/ibb")) {
|
||||
mXmppConnectionService.getJingleConnectionManager()
|
||||
.deliverIbbPacket(account, packet);
|
||||
} else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
|
||||
final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(packet);
|
||||
mXmppConnectionService.sendIqPacket(account, response, null);
|
||||
} else if (packet.hasChild("query","jabber:iq:version")) {
|
||||
} else if (packet.hasChild("query","jabber:iq:version") && isGet) {
|
||||
final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
|
||||
mXmppConnectionService.sendIqPacket(account,response,null);
|
||||
} else if (packet.hasChild("ping", "urn:xmpp:ping")) {
|
||||
} else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
|
||||
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
|
||||
mXmppConnectionService.sendIqPacket(account, response, null);
|
||||
} else if (packet.hasChild("time","urn:xmpp:time") && isGet) {
|
||||
final IqPacket response;
|
||||
if (mXmppConnectionService.useTorToConnect()) {
|
||||
response = packet.generateResponse(IqPacket.TYPE.ERROR);
|
||||
final Element error = response.addChild("error");
|
||||
error.setAttribute("type","cancel");
|
||||
error.addChild("not-allowed","urn:ietf:params:xml:ns:xmpp-stanzas");
|
||||
} else {
|
||||
response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
|
||||
}
|
||||
mXmppConnectionService.sendIqPacket(account,response, null);
|
||||
} else {
|
||||
if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
|
||||
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);
|
||||
|
|
|
@ -1,16 +1,22 @@
|
|||
package eu.siacs.conversations.parser;
|
||||
|
||||
import android.text.Html;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import eu.siacs.conversations.crypto.PgpDecryptionService;
|
||||
import net.java.otr4j.session.Session;
|
||||
import net.java.otr4j.session.SessionStatus;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.OtrService;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
|
@ -19,10 +25,13 @@ import eu.siacs.conversations.entities.Contact;
|
|||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.MucOptions;
|
||||
import eu.siacs.conversations.entities.Presence;
|
||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||
import eu.siacs.conversations.http.HttpConnectionManager;
|
||||
import eu.siacs.conversations.services.MessageArchiveService;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.Xmlns;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
|
||||
import eu.siacs.conversations.xmpp.chatstate.ChatState;
|
||||
|
@ -30,8 +39,10 @@ import eu.siacs.conversations.xmpp.jid.Jid;
|
|||
import eu.siacs.conversations.xmpp.pep.Avatar;
|
||||
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
|
||||
|
||||
public class MessageParser extends AbstractParser implements
|
||||
OnMessagePacketReceived {
|
||||
public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
|
||||
|
||||
private static final List<String> CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin","Adium","Trillian");
|
||||
|
||||
public MessageParser(XmppConnectionService service) {
|
||||
super(service);
|
||||
}
|
||||
|
@ -45,7 +56,7 @@ public class MessageParser extends AbstractParser implements
|
|||
conversation.setOutgoingChatState(state);
|
||||
if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
|
||||
mXmppConnectionService.markRead(conversation);
|
||||
account.activateGracePeriod();
|
||||
activateGracePeriod(account);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
|
@ -94,8 +105,16 @@ public class MessageParser extends AbstractParser implements
|
|||
conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
|
||||
return null;
|
||||
}
|
||||
if (clientMightSendHtml(conversation.getAccount(), from)) {
|
||||
Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": received OTR message from bad behaving client. escaping HTML…");
|
||||
body = Html.fromHtml(body).toString();
|
||||
}
|
||||
|
||||
final OtrService otrService = conversation.getAccount().getOtrService();
|
||||
Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED);
|
||||
finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey()));
|
||||
conversation.setLastReceivedOtrMessageId(null);
|
||||
|
||||
return finishedMessage;
|
||||
} catch (Exception e) {
|
||||
conversation.resetOtrSession();
|
||||
|
@ -103,33 +122,58 @@ public class MessageParser extends AbstractParser implements
|
|||
}
|
||||
}
|
||||
|
||||
private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
|
||||
Message finishedMessage = null;
|
||||
AxolotlService service = conversation.getAccount().getAxolotlService();
|
||||
XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
|
||||
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage);
|
||||
if(plaintextMessage != null) {
|
||||
finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
|
||||
finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint());
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
|
||||
private static boolean clientMightSendHtml(Account account, Jid from) {
|
||||
String resource = from.getResourcepart();
|
||||
if (resource == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return finishedMessage;
|
||||
Presence presence = account.getRoster().getContact(from).getPresences().getPresences().get(resource);
|
||||
ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult();
|
||||
if (disco == null) {
|
||||
return false;
|
||||
}
|
||||
return hasIdentityKnowForSendingHtml(disco.getIdentities());
|
||||
}
|
||||
|
||||
private Message parsePGPChat(final Conversation conversation, String pgpEncrypted, int status) {
|
||||
final Message message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
||||
PgpDecryptionService pgpDecryptionService = conversation.getAccount().getPgpDecryptionService();
|
||||
pgpDecryptionService.add(message);
|
||||
return message;
|
||||
private static boolean hasIdentityKnowForSendingHtml(List<ServiceDiscoveryResult.Identity> identities) {
|
||||
for(ServiceDiscoveryResult.Identity identity : identities) {
|
||||
if (identity.getName() != null) {
|
||||
if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status) {
|
||||
AxolotlService service = conversation.getAccount().getAxolotlService();
|
||||
XmppAxolotlMessage xmppAxolotlMessage;
|
||||
try {
|
||||
xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": invalid omemo message received "+e.getMessage());
|
||||
return null;
|
||||
}
|
||||
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage);
|
||||
if(plaintextMessage != null) {
|
||||
Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
|
||||
finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
|
||||
return finishedMessage;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private class Invite {
|
||||
Jid jid;
|
||||
String password;
|
||||
Invite(Jid jid, String password) {
|
||||
final Jid jid;
|
||||
final String password;
|
||||
final Contact inviter;
|
||||
Invite(Jid jid, String password, Contact inviter) {
|
||||
this.jid = jid;
|
||||
this.password = password;
|
||||
this.inviter = inviter;
|
||||
}
|
||||
|
||||
public boolean execute(Account account) {
|
||||
|
@ -138,7 +182,7 @@ public class MessageParser extends AbstractParser implements
|
|||
if (!conversation.getMucOptions().online()) {
|
||||
conversation.getMucOptions().setPassword(password);
|
||||
mXmppConnectionService.databaseBackend.updateConversation(conversation);
|
||||
mXmppConnectionService.joinMuc(conversation);
|
||||
mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
return true;
|
||||
|
@ -147,18 +191,22 @@ public class MessageParser extends AbstractParser implements
|
|||
}
|
||||
}
|
||||
|
||||
private Invite extractInvite(Element message) {
|
||||
private Invite extractInvite(Account account, Element message) {
|
||||
Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
|
||||
if (x != null) {
|
||||
Element invite = x.findChild("invite");
|
||||
if (invite != null) {
|
||||
Element pw = x.findChild("password");
|
||||
return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null);
|
||||
Jid from = invite.getAttributeAsJid("from");
|
||||
Contact contact = from == null ? null : account.getRoster().getContact(from);
|
||||
return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null, contact);
|
||||
}
|
||||
} else {
|
||||
x = message.findChild("x","jabber:x:conference");
|
||||
if (x != null) {
|
||||
return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password"));
|
||||
Jid from = message.getAttributeAsJid("from");
|
||||
Contact contact = from == null ? null : account.getRoster().getContact(from);
|
||||
return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password"),contact);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -167,7 +215,7 @@ public class MessageParser extends AbstractParser implements
|
|||
private static String extractStanzaId(Element packet, Jid by) {
|
||||
for(Element child : packet.getChildren()) {
|
||||
if (child.getName().equals("stanza-id")
|
||||
&& "urn:xmpp:sid:0".equals(child.getNamespace())
|
||||
&& Xmlns.STANZA_IDS.equals(child.getNamespace())
|
||||
&& by.equals(child.getAttributeAsJid("by"))) {
|
||||
return child.getAttribute("id");
|
||||
}
|
||||
|
@ -197,7 +245,7 @@ public class MessageParser extends AbstractParser implements
|
|||
mXmppConnectionService.updateConversationUi();
|
||||
mXmppConnectionService.updateRosterUi();
|
||||
}
|
||||
} else {
|
||||
} else if (mXmppConnectionService.isDataSaverDisabled()) {
|
||||
mXmppConnectionService.fetchAvatar(account, avatar);
|
||||
}
|
||||
}
|
||||
|
@ -212,9 +260,10 @@ public class MessageParser extends AbstractParser implements
|
|||
mXmppConnectionService.updateAccountUi();
|
||||
}
|
||||
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
|
||||
|
||||
Element item = items.findChild("item");
|
||||
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list ("+deviceIds+") update from "+ from + ", processing...");
|
||||
AxolotlService axolotlService = account.getAxolotlService();
|
||||
axolotlService.registerDevices(from, deviceIds);
|
||||
mXmppConnectionService.updateAccountUi();
|
||||
|
@ -225,19 +274,15 @@ public class MessageParser extends AbstractParser implements
|
|||
if (packet.getType() == MessagePacket.TYPE_ERROR) {
|
||||
Jid from = packet.getFrom();
|
||||
if (from != null) {
|
||||
Element error = packet.findChild("error");
|
||||
String text = error == null ? null : error.findChildContent("text");
|
||||
if (text != null) {
|
||||
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text);
|
||||
} else if (error != null) {
|
||||
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error);
|
||||
}
|
||||
Message message = mXmppConnectionService.markMessage(account,
|
||||
from.toBareJid(),
|
||||
packet.getId(),
|
||||
Message.STATUS_SEND_FAILED);
|
||||
if (message != null && message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||
message.getConversation().endOtrIfNeeded();
|
||||
Message.STATUS_SEND_FAILED,
|
||||
extractErrorMessage(packet));
|
||||
if (message != null) {
|
||||
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||
message.getConversation().endOtrIfNeeded();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
@ -271,7 +316,7 @@ public class MessageParser extends AbstractParser implements
|
|||
packet = f.first;
|
||||
isForwarded = true;
|
||||
serverMsgId = result.getAttribute("id");
|
||||
query.incrementTotalCount();
|
||||
query.incrementMessageCount();
|
||||
} else if (query != null) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender");
|
||||
return;
|
||||
|
@ -292,17 +337,22 @@ public class MessageParser extends AbstractParser implements
|
|||
}
|
||||
|
||||
if (timestamp == null) {
|
||||
timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
|
||||
timestamp = AbstractParser.parseTimestamp(original,AbstractParser.parseTimestamp(packet));
|
||||
}
|
||||
final String body = packet.getBody();
|
||||
final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
|
||||
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
|
||||
final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
|
||||
final Element oob = packet.findChild("x", "jabber:x:oob");
|
||||
final boolean isOob = oob!= null && body != null && body.equals(oob.findChildContent("url"));
|
||||
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
|
||||
final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
|
||||
int status;
|
||||
final Jid counterpart;
|
||||
final Jid to = packet.getTo();
|
||||
final Jid from = packet.getFrom();
|
||||
final String remoteMsgId = packet.getId();
|
||||
boolean notify = false;
|
||||
|
||||
if (from == null) {
|
||||
Log.d(Config.LOGTAG,"no from in: "+packet.toString());
|
||||
|
@ -310,7 +360,7 @@ public class MessageParser extends AbstractParser implements
|
|||
}
|
||||
|
||||
boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
|
||||
boolean isProperlyAddressed = (to != null ) && (!to.isBareJid() || account.countPresences() == 1);
|
||||
boolean isProperlyAddressed = (to != null ) && (!to.isBareJid() || account.countPresences() == 0);
|
||||
boolean isMucStatusMessage = from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
|
||||
if (packet.fromAccount(account)) {
|
||||
status = Message.STATUS_SEND;
|
||||
|
@ -320,20 +370,24 @@ public class MessageParser extends AbstractParser implements
|
|||
counterpart = from;
|
||||
}
|
||||
|
||||
Invite invite = extractInvite(packet);
|
||||
Invite invite = extractInvite(account, packet);
|
||||
if (invite != null && invite.execute(account)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), packet)) {
|
||||
if (!isTypeGroupChat
|
||||
&& query == null
|
||||
&& extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), packet)) {
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
|
||||
if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
|
||||
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat, query);
|
||||
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
|
||||
if (isTypeGroupChat) {
|
||||
if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
|
||||
status = Message.STATUS_SEND_RECEIVED;
|
||||
isCarbon = true; //not really carbon but received from another resource
|
||||
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status)) {
|
||||
return;
|
||||
} else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
|
||||
|
@ -347,29 +401,55 @@ public class MessageParser extends AbstractParser implements
|
|||
status = Message.STATUS_RECEIVED;
|
||||
}
|
||||
}
|
||||
Message message;
|
||||
if (body != null && body.startsWith("?OTR")) {
|
||||
if (!isForwarded && !isTypeGroupChat && isProperlyAddressed) {
|
||||
final Message message;
|
||||
if (body != null && body.startsWith("?OTR") && Config.supportOtr()) {
|
||||
if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) {
|
||||
message = parseOtrChat(body, from, remoteMsgId, conversation);
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring OTR message from "+from+" isForwarded="+Boolean.toString(isForwarded)+", isProperlyAddressed="+Boolean.valueOf(isProperlyAddressed));
|
||||
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
|
||||
}
|
||||
} else if (pgpEncrypted != null) {
|
||||
message = parsePGPChat(conversation, pgpEncrypted, status);
|
||||
} else if (axolotlEncrypted != null) {
|
||||
message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
|
||||
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
|
||||
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
|
||||
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
|
||||
Jid origin;
|
||||
if (conversationMultiMode) {
|
||||
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
|
||||
origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
|
||||
if (origin == null) {
|
||||
Log.d(Config.LOGTAG,"axolotl message in non anonymous conference received");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
origin = from;
|
||||
}
|
||||
message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status);
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
if (conversationMultiMode) {
|
||||
message.setTrueCounterpart(origin);
|
||||
}
|
||||
} else {
|
||||
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
|
||||
}
|
||||
|
||||
if (serverMsgId == null) {
|
||||
serverMsgId = extractStanzaId(packet, isTypeGroupChat ? conversation.getJid().toBareJid() : account.getServer());
|
||||
final Jid by;
|
||||
final boolean safeToExtract;
|
||||
if (isTypeGroupChat) {
|
||||
by = conversation.getJid().toBareJid();
|
||||
safeToExtract = true; //conversation.getMucOptions().hasFeature(Xmlns.STANZA_IDS);
|
||||
} else {
|
||||
by = account.getJid().toBareJid();
|
||||
safeToExtract = true; //account.getXmppConnection().getFeatures().stanzaIds();
|
||||
}
|
||||
if (safeToExtract) {
|
||||
serverMsgId = extractStanzaId(packet, by);
|
||||
}
|
||||
}
|
||||
|
||||
message.setCounterpart(counterpart);
|
||||
|
@ -377,19 +457,76 @@ public class MessageParser extends AbstractParser implements
|
|||
message.setServerMsgId(serverMsgId);
|
||||
message.setCarbon(isCarbon);
|
||||
message.setTime(timestamp);
|
||||
message.setOob(isOob);
|
||||
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
|
||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||
Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart());
|
||||
message.setTrueCounterpart(trueCounterpart);
|
||||
if (trueCounterpart != null) {
|
||||
updateLastseen(timestamp, account, trueCounterpart, false);
|
||||
if (conversationMultiMode) {
|
||||
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
|
||||
Jid trueCounterpart;
|
||||
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
|
||||
trueCounterpart = message.getTrueCounterpart();
|
||||
} else if (Config.PARSE_REAL_JID_FROM_MUC_MAM) {
|
||||
trueCounterpart = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
|
||||
} else {
|
||||
trueCounterpart = fallback;
|
||||
}
|
||||
if (trueCounterpart != null && trueCounterpart.toBareJid().equals(account.getJid().toBareJid())) {
|
||||
status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
|
||||
}
|
||||
message.setStatus(status);
|
||||
message.setTrueCounterpart(trueCounterpart);
|
||||
if (!isTypeGroupChat) {
|
||||
message.setType(Message.TYPE_PRIVATE);
|
||||
}
|
||||
} else {
|
||||
updateLastseen(timestamp, account, packet.getFrom(), true);
|
||||
updateLastseen(account, from);
|
||||
}
|
||||
|
||||
if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
|
||||
Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
|
||||
counterpart,
|
||||
message.getStatus() == Message.STATUS_RECEIVED,
|
||||
message.isCarbon());
|
||||
if (replacedMessage != null) {
|
||||
final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
|
||||
|| replacedMessage.getFingerprint().equals(message.getFingerprint());
|
||||
final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
|
||||
&& replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
|
||||
final boolean duplicate = conversation.hasDuplicateMessage(message);
|
||||
if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode) && !duplicate) {
|
||||
Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
|
||||
synchronized (replacedMessage) {
|
||||
final String uuid = replacedMessage.getUuid();
|
||||
replacedMessage.setUuid(UUID.randomUUID().toString());
|
||||
replacedMessage.setBody(message.getBody());
|
||||
replacedMessage.setEdited(replacedMessage.getRemoteMsgId());
|
||||
replacedMessage.setRemoteMsgId(remoteMsgId);
|
||||
replacedMessage.setEncryption(message.getEncryption());
|
||||
if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
|
||||
replacedMessage.markUnread();
|
||||
}
|
||||
mXmppConnectionService.updateMessage(replacedMessage, uuid);
|
||||
mXmppConnectionService.getNotificationService().updateNotification(false);
|
||||
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
|
||||
sendMessageReceipts(account, packet);
|
||||
}
|
||||
if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||
conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
|
||||
conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received message correction but verification didn't check out");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
|
||||
if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping message from "+message.getCounterpart().toString()+" because it was sent prior to our deletion date");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean checkForDuplicates = query != null
|
||||
|| (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay"))
|
||||
|| message.getType() == Message.TYPE_PRIVATE;
|
||||
|
@ -398,40 +535,38 @@ public class MessageParser extends AbstractParser implements
|
|||
return;
|
||||
}
|
||||
|
||||
conversation.add(message);
|
||||
if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
|
||||
conversation.prepend(message);
|
||||
} else {
|
||||
conversation.add(message);
|
||||
}
|
||||
|
||||
if (query == null || query.getWith() == null) { //either no mam or catchup
|
||||
if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
|
||||
mXmppConnectionService.markRead(conversation);
|
||||
if (query == null) {
|
||||
account.activateGracePeriod();
|
||||
activateGracePeriod(account);
|
||||
}
|
||||
} else {
|
||||
message.markUnread();
|
||||
notify = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (query != null) {
|
||||
query.incrementMessageCount();
|
||||
} else {
|
||||
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
|
||||
notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
|
||||
}
|
||||
|
||||
if (query == null) {
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
|
||||
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
|
||||
ArrayList<String> receiptsNamespaces = new ArrayList<>();
|
||||
if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
|
||||
receiptsNamespaces.add("urn:xmpp:chat-markers:0");
|
||||
}
|
||||
if (packet.hasChild("request", "urn:xmpp:receipts")) {
|
||||
receiptsNamespaces.add("urn:xmpp:receipts");
|
||||
}
|
||||
if (receiptsNamespaces.size() > 0) {
|
||||
MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
|
||||
packet,
|
||||
receiptsNamespaces,
|
||||
packet.getType());
|
||||
mXmppConnectionService.sendMessagePacket(account, receipt);
|
||||
}
|
||||
if (mXmppConnectionService.confirmMessages()
|
||||
&& message.trusted()
|
||||
&& remoteMsgId != null
|
||||
&& !isForwarded
|
||||
&& !isTypeGroupChat) {
|
||||
sendMessageReceipts(account, packet);
|
||||
}
|
||||
|
||||
if (message.getStatus() == Message.STATUS_RECEIVED
|
||||
|
@ -441,13 +576,11 @@ public class MessageParser extends AbstractParser implements
|
|||
conversation.endOtrIfNeeded();
|
||||
}
|
||||
|
||||
if (message.getEncryption() == Message.ENCRYPTION_NONE || mXmppConnectionService.saveEncryptedMessages()) {
|
||||
mXmppConnectionService.databaseBackend.createMessage(message);
|
||||
}
|
||||
mXmppConnectionService.databaseBackend.createMessage(message);
|
||||
final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
|
||||
if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER && manager.getAutoAcceptFileSize() > 0) {
|
||||
manager.createNewDownloadConnection(message);
|
||||
} else if (!message.isRead()) {
|
||||
} else if (notify) {
|
||||
if (query == null) {
|
||||
mXmppConnectionService.getNotificationService().push(message);
|
||||
} else if (query.getWith() == null) { // mam catchup
|
||||
|
@ -455,8 +588,8 @@ public class MessageParser extends AbstractParser implements
|
|||
}
|
||||
}
|
||||
} else if (!packet.hasChild("body")){ //no body
|
||||
Conversation conversation = mXmppConnectionService.find(account, from.toBareJid());
|
||||
if (isTypeGroupChat) {
|
||||
Conversation conversation = mXmppConnectionService.find(account, from.toBareJid());
|
||||
if (packet.hasChild("subject")) {
|
||||
if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
|
||||
conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
|
||||
|
@ -472,18 +605,46 @@ public class MessageParser extends AbstractParser implements
|
|||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (conversation != null && isMucStatusMessage) {
|
||||
for (Element child : mucUserElement.getChildren()) {
|
||||
if (child.getName().equals("status")
|
||||
&& MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED.equals(child.getAttribute("code"))) {
|
||||
mXmppConnectionService.fetchConferenceConfiguration(conversation);
|
||||
}
|
||||
if (conversation != null && mucUserElement != null && from.isBareJid()) {
|
||||
for (Element child : mucUserElement.getChildren()) {
|
||||
if ("status".equals(child.getName())) {
|
||||
try {
|
||||
int code = Integer.parseInt(child.getAttribute("code"));
|
||||
if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
|
||||
mXmppConnectionService.fetchConferenceConfiguration(conversation);
|
||||
break;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
//ignored
|
||||
}
|
||||
} else if ("item".equals(child.getName())) {
|
||||
MucOptions.User user = AbstractParser.parseItem(conversation,child);
|
||||
Log.d(Config.LOGTAG,account.getJid()+": changing affiliation for "
|
||||
+user.getRealJid()+" to "+user.getAffiliation()+" in "
|
||||
+conversation.getJid().toBareJid());
|
||||
if (!user.realJidMatchesAccount()) {
|
||||
conversation.getMucOptions().updateUser(user);
|
||||
mXmppConnectionService.getAvatarService().clear(conversation);
|
||||
mXmppConnectionService.updateMucRosterUi();
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
|
||||
Jid jid = user.getRealJid();
|
||||
List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
|
||||
if (cryptoTargets.remove(user.getRealJid())) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": removed "+jid+" from crypto targets of "+conversation.getName());
|
||||
conversation.setAcceptedCryptoTargets(cryptoTargets);
|
||||
mXmppConnectionService.updateConversation(conversation);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
|
||||
if (received == null) {
|
||||
received = packet.findChild("received", "urn:xmpp:receipts");
|
||||
|
@ -499,7 +660,6 @@ public class MessageParser extends AbstractParser implements
|
|||
mXmppConnectionService.markRead(conversation);
|
||||
}
|
||||
} else {
|
||||
updateLastseen(timestamp, account, packet.getFrom(), true);
|
||||
final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), displayed.getAttribute("id"), Message.STATUS_SEND_DISPLAYED);
|
||||
Message message = displayedMessage == null ? null : displayedMessage.prev();
|
||||
while (message != null
|
||||
|
@ -511,9 +671,9 @@ public class MessageParser extends AbstractParser implements
|
|||
}
|
||||
}
|
||||
|
||||
Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event");
|
||||
Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
|
||||
if (event != null) {
|
||||
parseEvent(event, from, account);
|
||||
parseEvent(event, original.getFrom(), account);
|
||||
}
|
||||
|
||||
String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick");
|
||||
|
@ -522,4 +682,35 @@ public class MessageParser extends AbstractParser implements
|
|||
contact.setPresenceName(nick);
|
||||
}
|
||||
}
|
||||
|
||||
private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
|
||||
final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
|
||||
Jid result = item == null ? null : item.getAttributeAsJid("jid");
|
||||
return result != null ? result : fallback;
|
||||
}
|
||||
|
||||
private void sendMessageReceipts(Account account, MessagePacket packet) {
|
||||
ArrayList<String> receiptsNamespaces = new ArrayList<>();
|
||||
if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
|
||||
receiptsNamespaces.add("urn:xmpp:chat-markers:0");
|
||||
}
|
||||
if (packet.hasChild("request", "urn:xmpp:receipts")) {
|
||||
receiptsNamespaces.add("urn:xmpp:receipts");
|
||||
}
|
||||
if (receiptsNamespaces.size() > 0) {
|
||||
MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
|
||||
packet,
|
||||
receiptsNamespaces,
|
||||
packet.getType());
|
||||
mXmppConnectionService.sendMessagePacket(account, receipt);
|
||||
}
|
||||
}
|
||||
|
||||
private static SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss");
|
||||
|
||||
private void activateGracePeriod(Account account) {
|
||||
long duration = mXmppConnectionService.getPreferences().getLong("race_period_length", 144) * 1000;
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": activating grace period till "+TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
|
||||
account.activateGracePeriod(duration);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package eu.siacs.conversations.parser;
|
|||
|
||||
import android.util.Log;
|
||||
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -13,7 +14,8 @@ import eu.siacs.conversations.entities.Contact;
|
|||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.MucOptions;
|
||||
import eu.siacs.conversations.entities.Presences;
|
||||
import eu.siacs.conversations.entities.Presence;
|
||||
import eu.siacs.conversations.generator.IqGenerator;
|
||||
import eu.siacs.conversations.generator.PresenceGenerator;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.xml.Element;
|
||||
|
@ -36,10 +38,9 @@ public class PresenceParser extends AbstractParser implements
|
|||
boolean before = mucOptions.online();
|
||||
int count = mucOptions.getUserCount();
|
||||
final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5);
|
||||
processConferencePresence(packet, mucOptions);
|
||||
processConferencePresence(packet, conversation);
|
||||
final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5);
|
||||
if (!tileUserAfter.equals(tileUserBefore)) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": update tiles for "+conversation.getName());
|
||||
mXmppConnectionService.getAvatarService().clear(mucOptions);
|
||||
}
|
||||
if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUserCount())) {
|
||||
|
@ -50,7 +51,8 @@ public class PresenceParser extends AbstractParser implements
|
|||
}
|
||||
}
|
||||
|
||||
private void processConferencePresence(PresencePacket packet, MucOptions mucOptions) {
|
||||
private void processConferencePresence(PresencePacket packet, Conversation conversation) {
|
||||
MucOptions mucOptions = conversation.getMucOptions();
|
||||
final Jid from = packet.getFrom();
|
||||
if (!from.isBareJid()) {
|
||||
final String type = packet.getAttribute("type");
|
||||
|
@ -61,11 +63,8 @@ public class PresenceParser extends AbstractParser implements
|
|||
if (x != null) {
|
||||
Element item = x.findChild("item");
|
||||
if (item != null && !from.isBareJid()) {
|
||||
mucOptions.setError(MucOptions.ERROR_NO_ERROR);
|
||||
MucOptions.User user = new MucOptions.User(mucOptions,from);
|
||||
user.setAffiliation(item.getAttribute("affiliation"));
|
||||
user.setRole(item.getAttribute("role"));
|
||||
user.setJid(item.getAttributeAsJid("jid"));
|
||||
mucOptions.setError(MucOptions.Error.NONE);
|
||||
MucOptions.User user = parseItem(conversation, item, from);
|
||||
if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) {
|
||||
mucOptions.setOnline();
|
||||
mucOptions.setSelf(user);
|
||||
|
@ -76,7 +75,16 @@ public class PresenceParser extends AbstractParser implements
|
|||
mucOptions.mNickChangingInProgress = false;
|
||||
}
|
||||
} else {
|
||||
mucOptions.addUser(user);
|
||||
mucOptions.updateUser(user);
|
||||
}
|
||||
if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) {
|
||||
Log.d(Config.LOGTAG,mucOptions.getAccount().getJid().toBareJid()
|
||||
+": room '"
|
||||
+mucOptions.getConversation().getJid().toBareJid()
|
||||
+"' created. pushing default configuration");
|
||||
mXmppConnectionService.pushConferenceConfiguration(mucOptions.getConversation(),
|
||||
IqGenerator.defaultRoomConfiguration(),
|
||||
null);
|
||||
}
|
||||
if (mXmppConnectionService.getPgpEngine() != null) {
|
||||
Element signed = packet.findChild("x", "jabber:x:signed");
|
||||
|
@ -95,7 +103,7 @@ public class PresenceParser extends AbstractParser implements
|
|||
if (user.setAvatar(avatar)) {
|
||||
mXmppConnectionService.getAvatarService().clear(user);
|
||||
}
|
||||
} else {
|
||||
} else if (mXmppConnectionService.isDataSaverDisabled()) {
|
||||
mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar);
|
||||
}
|
||||
}
|
||||
|
@ -107,17 +115,25 @@ public class PresenceParser extends AbstractParser implements
|
|||
if (codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) {
|
||||
mucOptions.mNickChangingInProgress = true;
|
||||
} else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
|
||||
mucOptions.setError(MucOptions.KICKED_FROM_ROOM);
|
||||
mucOptions.setError(MucOptions.Error.KICKED);
|
||||
} else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) {
|
||||
mucOptions.setError(MucOptions.ERROR_BANNED);
|
||||
mucOptions.setError(MucOptions.Error.BANNED);
|
||||
} else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) {
|
||||
mucOptions.setError(MucOptions.ERROR_MEMBERS_ONLY);
|
||||
mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
|
||||
} else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) {
|
||||
mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
|
||||
} else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) {
|
||||
mucOptions.setError(MucOptions.Error.SHUTDOWN);
|
||||
} else {
|
||||
mucOptions.setError(MucOptions.ERROR_UNKNOWN);
|
||||
mucOptions.setError(MucOptions.Error.UNKNOWN);
|
||||
Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
|
||||
}
|
||||
} else if (!from.isBareJid()){
|
||||
MucOptions.User user = mucOptions.deleteUser(from.getResourcepart());
|
||||
Element item = x.findChild("item");
|
||||
if (item != null) {
|
||||
mucOptions.updateUser(parseItem(conversation, item, from));
|
||||
}
|
||||
MucOptions.User user = mucOptions.deleteUser(from);
|
||||
if (user != null) {
|
||||
mXmppConnectionService.getAvatarService().clear(user);
|
||||
}
|
||||
|
@ -130,14 +146,14 @@ public class PresenceParser extends AbstractParser implements
|
|||
mucOptions.onRenameListener.onFailure();
|
||||
}
|
||||
} else {
|
||||
mucOptions.setError(MucOptions.ERROR_NICK_IN_USE);
|
||||
mucOptions.setError(MucOptions.Error.NICK_IN_USE);
|
||||
}
|
||||
} else if (error != null && error.hasChild("not-authorized")) {
|
||||
mucOptions.setError(MucOptions.ERROR_PASSWORD_REQUIRED);
|
||||
mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED);
|
||||
} else if (error != null && error.hasChild("forbidden")) {
|
||||
mucOptions.setError(MucOptions.ERROR_BANNED);
|
||||
mucOptions.setError(MucOptions.Error.BANNED);
|
||||
} else if (error != null && error.hasChild("registration-required")) {
|
||||
mucOptions.setError(MucOptions.ERROR_MEMBERS_ONLY);
|
||||
mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -161,29 +177,58 @@ public class PresenceParser extends AbstractParser implements
|
|||
public void parseContactPresence(final PresencePacket packet, final Account account) {
|
||||
final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
|
||||
final Jid from = packet.getFrom();
|
||||
if (from == null) {
|
||||
if (from == null || from.equals(account.getJid())) {
|
||||
return;
|
||||
}
|
||||
final String type = packet.getAttribute("type");
|
||||
final Contact contact = account.getRoster().getContact(from);
|
||||
if (type == null) {
|
||||
String presence = from.isBareJid() ? "" : from.getResourcepart();
|
||||
final String resource = from.isBareJid() ? "" : from.getResourcepart();
|
||||
contact.setPresenceName(packet.findChildContent("nick", "http://jabber.org/protocol/nick"));
|
||||
Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
|
||||
if (avatar != null && !contact.isSelf()) {
|
||||
if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) {
|
||||
avatar.owner = from.toBareJid();
|
||||
if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
|
||||
if (contact.setAvatar(avatar)) {
|
||||
if (avatar.owner.equals(account.getJid().toBareJid())) {
|
||||
account.setAvatar(avatar.getFilename());
|
||||
mXmppConnectionService.databaseBackend.updateAccount(account);
|
||||
mXmppConnectionService.getAvatarService().clear(account);
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
mXmppConnectionService.updateAccountUi();
|
||||
} else if (contact.setAvatar(avatar)) {
|
||||
mXmppConnectionService.getAvatarService().clear(contact);
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
mXmppConnectionService.updateRosterUi();
|
||||
}
|
||||
} else {
|
||||
} else if (mXmppConnectionService.isDataSaverDisabled()){
|
||||
mXmppConnectionService.fetchAvatar(account, avatar);
|
||||
}
|
||||
}
|
||||
int sizeBefore = contact.getPresences().size();
|
||||
contact.updatePresence(presence, Presences.parseShow(packet.findChild("show")));
|
||||
|
||||
final String show = packet.findChildContent("show");
|
||||
final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps");
|
||||
final String message = packet.findChildContent("status");
|
||||
final Presence presence = Presence.parse(show, caps, message);
|
||||
contact.updatePresence(resource, presence);
|
||||
if (presence.hasCaps()) {
|
||||
mXmppConnectionService.fetchCaps(account, from, presence);
|
||||
}
|
||||
|
||||
final Element idle = packet.findChild("idle","urn:xmpp:idle:1");
|
||||
if (idle != null) {
|
||||
contact.flagInactive();
|
||||
String since = idle.getAttribute("since");
|
||||
try {
|
||||
contact.setLastseen(AbstractParser.parseTimestamp(since));
|
||||
} catch (NullPointerException | ParseException e) {
|
||||
contact.setLastseen(System.currentTimeMillis());
|
||||
}
|
||||
} else {
|
||||
contact.flagActive();
|
||||
contact.setLastseen(AbstractParser.parseTimestamp(packet));
|
||||
}
|
||||
|
||||
PgpEngine pgp = mXmppConnectionService.getPgpEngine();
|
||||
Element x = packet.findChild("x", "jabber:x:signed");
|
||||
if (pgp != null && x != null) {
|
||||
|
@ -192,7 +237,6 @@ public class PresenceParser extends AbstractParser implements
|
|||
contact.setPgpKeyId(pgp.fetchKeyId(account, msg, x.getContent()));
|
||||
}
|
||||
boolean online = sizeBefore < contact.getPresences().size();
|
||||
updateLastseen(packet, account, false);
|
||||
mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online);
|
||||
} else if (type.equals("unavailable")) {
|
||||
if (from.isBareJid()) {
|
||||
|
|
|
@ -7,10 +7,12 @@ import android.database.DatabaseUtils;
|
|||
import android.database.sqlite.SQLiteCantOpenDatabaseException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
import android.os.Environment;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.whispersystems.libaxolotl.AxolotlAddress;
|
||||
import org.whispersystems.libaxolotl.IdentityKey;
|
||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||
|
@ -20,27 +22,34 @@ import org.whispersystems.libaxolotl.state.SessionRecord;
|
|||
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.cert.CertificateEncodingException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import org.json.JSONException;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.PresenceTemplate;
|
||||
import eu.siacs.conversations.entities.Roster;
|
||||
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
|
||||
import eu.siacs.conversations.utils.MimeUtils;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
||||
|
@ -49,7 +58,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
private static DatabaseBackend instance = null;
|
||||
|
||||
private static final String DATABASE_NAME = "history";
|
||||
private static final int DATABASE_VERSION = 22;
|
||||
private static final int DATABASE_VERSION = 34;
|
||||
|
||||
private static String CREATE_CONTATCS_STATEMENT = "create table "
|
||||
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
|
||||
|
@ -63,6 +72,22 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
|
||||
+ Contact.JID + ") ON CONFLICT REPLACE);";
|
||||
|
||||
private static String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table "
|
||||
+ ServiceDiscoveryResult.TABLENAME + "("
|
||||
+ ServiceDiscoveryResult.HASH + " TEXT, "
|
||||
+ ServiceDiscoveryResult.VER + " TEXT, "
|
||||
+ ServiceDiscoveryResult.RESULT + " TEXT, "
|
||||
+ "UNIQUE(" + ServiceDiscoveryResult.HASH + ", "
|
||||
+ ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);";
|
||||
|
||||
private static String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE "
|
||||
+ PresenceTemplate.TABELNAME + "("
|
||||
+ PresenceTemplate.UUID + " TEXT, "
|
||||
+ PresenceTemplate.LAST_USED + " NUMBER,"
|
||||
+ PresenceTemplate.MESSAGE + " TEXT,"
|
||||
+ PresenceTemplate.STATUS + " TEXT,"
|
||||
+ "UNIQUE("+PresenceTemplate.MESSAGE + "," +PresenceTemplate.STATUS+") ON CONFLICT REPLACE);";
|
||||
|
||||
private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
|
||||
+ SQLiteAxolotlStore.PREKEY_TABLENAME + "("
|
||||
+ SQLiteAxolotlStore.ACCOUNT + " TEXT, "
|
||||
|
@ -108,7 +133,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ SQLiteAxolotlStore.OWN + " INTEGER, "
|
||||
+ SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
|
||||
+ SQLiteAxolotlStore.CERTIFICATE + " BLOB, "
|
||||
+ SQLiteAxolotlStore.TRUSTED + " INTEGER, "
|
||||
+ SQLiteAxolotlStore.TRUST + " TEXT, "
|
||||
+ SQLiteAxolotlStore.ACTIVE + " NUMBER, "
|
||||
+ SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER,"
|
||||
+ SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
|
||||
+ SQLiteAxolotlStore.ACCOUNT
|
||||
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
|
||||
|
@ -118,6 +145,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ ") ON CONFLICT IGNORE"
|
||||
+ ");";
|
||||
|
||||
private static String START_TIMES_TABLE = "start_times";
|
||||
|
||||
private static String CREATE_START_TIMES_TABLE = "create table "+START_TIMES_TABLE+" (timestamp NUMBER);";
|
||||
|
||||
private static String CREATE_MESSAGE_TIME_INDEX = "create INDEX message_time_index ON "+Message.TABLENAME+"("+Message.TIME_SENT+")";
|
||||
|
||||
private DatabaseBackend(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
@ -125,13 +158,19 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL("PRAGMA foreign_keys=ON;");
|
||||
db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID
|
||||
+ " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT,"
|
||||
+ Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT,"
|
||||
db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID+ " TEXT PRIMARY KEY,"
|
||||
+ Account.USERNAME + " TEXT,"
|
||||
+ Account.SERVER + " TEXT,"
|
||||
+ Account.PASSWORD + " TEXT,"
|
||||
+ Account.DISPLAY_NAME + " TEXT, "
|
||||
+ Account.ROSTERVERSION + " TEXT," + Account.OPTIONS
|
||||
+ " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS
|
||||
+ " TEXT, " + Account.HOSTNAME + " TEXT, " + Account.PORT + " NUMBER DEFAULT 5222)");
|
||||
+ Account.STATUS + " TEXT,"
|
||||
+ Account.STATUS_MESSAGE + " TEXT,"
|
||||
+ Account.ROSTERVERSION + " TEXT,"
|
||||
+ Account.OPTIONS + " NUMBER, "
|
||||
+ Account.AVATAR + " TEXT, "
|
||||
+ Account.KEYS + " TEXT, "
|
||||
+ Account.HOSTNAME + " TEXT, "
|
||||
+ Account.PORT + " NUMBER DEFAULT 5222)");
|
||||
db.execSQL("create table " + Conversation.TABLENAME + " ("
|
||||
+ Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
|
||||
+ " TEXT, " + Conversation.CONTACT + " TEXT, "
|
||||
|
@ -151,17 +190,23 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ Message.SERVER_MSG_ID + " TEXT, "
|
||||
+ Message.FINGERPRINT + " TEXT, "
|
||||
+ Message.CARBON + " INTEGER, "
|
||||
+ Message.EDITED + " TEXT, "
|
||||
+ Message.READ + " NUMBER DEFAULT 1, "
|
||||
+ Message.OOB + " INTEGER, "
|
||||
+ Message.ERROR_MESSAGE + " TEXT,"
|
||||
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
|
||||
+ Message.CONVERSATION + ") REFERENCES "
|
||||
+ Conversation.TABLENAME + "(" + Conversation.UUID
|
||||
+ ") ON DELETE CASCADE);");
|
||||
|
||||
db.execSQL(CREATE_MESSAGE_TIME_INDEX);
|
||||
db.execSQL(CREATE_CONTATCS_STATEMENT);
|
||||
db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
|
||||
db.execSQL(CREATE_SESSIONS_STATEMENT);
|
||||
db.execSQL(CREATE_PREKEYS_STATEMENT);
|
||||
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
|
||||
db.execSQL(CREATE_IDENTITIES_STATEMENT);
|
||||
db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
|
||||
db.execSQL(CREATE_START_TIMES_TABLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -221,91 +266,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
|
||||
}
|
||||
if (oldVersion < 14 && newVersion >= 14) {
|
||||
// migrate db to new, canonicalized JID domainpart representation
|
||||
|
||||
// Conversation table
|
||||
Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]);
|
||||
while (cursor.moveToNext()) {
|
||||
String newJid;
|
||||
try {
|
||||
newJid = Jid.fromString(
|
||||
cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
|
||||
).toString();
|
||||
} catch (InvalidJidException ignored) {
|
||||
Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID "
|
||||
+ cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
|
||||
+ ": " + ignored + ". Skipping...");
|
||||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
newJid,
|
||||
cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
|
||||
};
|
||||
db.execSQL("update " + Conversation.TABLENAME
|
||||
+ " set " + Conversation.CONTACTJID + " = ? "
|
||||
+ " where " + Conversation.UUID + " = ?", updateArgs);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
// Contact table
|
||||
cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]);
|
||||
while (cursor.moveToNext()) {
|
||||
String newJid;
|
||||
try {
|
||||
newJid = Jid.fromString(
|
||||
cursor.getString(cursor.getColumnIndex(Contact.JID))
|
||||
).toString();
|
||||
} catch (InvalidJidException ignored) {
|
||||
Log.e(Config.LOGTAG, "Failed to migrate Contact JID "
|
||||
+ cursor.getString(cursor.getColumnIndex(Contact.JID))
|
||||
+ ": " + ignored + ". Skipping...");
|
||||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
newJid,
|
||||
cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
|
||||
cursor.getString(cursor.getColumnIndex(Contact.JID)),
|
||||
};
|
||||
db.execSQL("update " + Contact.TABLENAME
|
||||
+ " set " + Contact.JID + " = ? "
|
||||
+ " where " + Contact.ACCOUNT + " = ? "
|
||||
+ " AND " + Contact.JID + " = ?", updateArgs);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
// Account table
|
||||
cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]);
|
||||
while (cursor.moveToNext()) {
|
||||
String newServer;
|
||||
try {
|
||||
newServer = Jid.fromParts(
|
||||
cursor.getString(cursor.getColumnIndex(Account.USERNAME)),
|
||||
cursor.getString(cursor.getColumnIndex(Account.SERVER)),
|
||||
"mobile"
|
||||
).getDomainpart();
|
||||
} catch (InvalidJidException ignored) {
|
||||
Log.e(Config.LOGTAG, "Failed to migrate Account SERVER "
|
||||
+ cursor.getString(cursor.getColumnIndex(Account.SERVER))
|
||||
+ ": " + ignored + ". Skipping...");
|
||||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
newServer,
|
||||
cursor.getString(cursor.getColumnIndex(Account.UUID)),
|
||||
};
|
||||
db.execSQL("update " + Account.TABLENAME
|
||||
+ " set " + Account.SERVER + " = ? "
|
||||
+ " where " + Account.UUID + " = ?", updateArgs);
|
||||
}
|
||||
cursor.close();
|
||||
canonicalizeJids(db);
|
||||
}
|
||||
if (oldVersion < 15 && newVersion >= 15) {
|
||||
recreateAxolotlDb(db);
|
||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
|
||||
+ Message.FINGERPRINT + " TEXT");
|
||||
} else if (oldVersion < 22 && newVersion >= 22) {
|
||||
db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE);
|
||||
}
|
||||
if (oldVersion < 16 && newVersion >= 16) {
|
||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
|
||||
|
@ -318,6 +286,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT");
|
||||
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222");
|
||||
}
|
||||
if (oldVersion < 26 && newVersion >= 26) {
|
||||
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT");
|
||||
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT");
|
||||
}
|
||||
/* Any migrations that alter the Account table need to happen BEFORE this migration, as it
|
||||
* depends on account de-serialization.
|
||||
*/
|
||||
|
@ -329,11 +301,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
continue;
|
||||
}
|
||||
int ownDeviceId = Integer.valueOf(ownDeviceIdString);
|
||||
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), ownDeviceId);
|
||||
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownDeviceId);
|
||||
deleteSession(db, account, ownAddress);
|
||||
IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account);
|
||||
if (identityKeyPair != null) {
|
||||
setIdentityKeyTrust(db, account, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.TRUSTED);
|
||||
String[] selectionArgs = {
|
||||
account.getUuid(),
|
||||
identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", "")
|
||||
};
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(SQLiteAxolotlStore.TRUSTED, 2);
|
||||
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
|
||||
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
|
||||
+ SQLiteAxolotlStore.FINGERPRINT + " = ? ",
|
||||
selectionArgs);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair");
|
||||
}
|
||||
|
@ -352,9 +333,195 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 22 && newVersion >= 22) {
|
||||
db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE);
|
||||
if (oldVersion < 23 && newVersion >= 23) {
|
||||
db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
|
||||
}
|
||||
|
||||
if (oldVersion < 24 && newVersion >= 24) {
|
||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
|
||||
}
|
||||
|
||||
if (oldVersion < 25 && newVersion >= 25) {
|
||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER");
|
||||
}
|
||||
|
||||
if (oldVersion < 26 && newVersion >= 26) {
|
||||
db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
|
||||
}
|
||||
|
||||
if (oldVersion < 27 && newVersion >= 27) {
|
||||
db.execSQL("DELETE FROM "+ServiceDiscoveryResult.TABLENAME);
|
||||
}
|
||||
|
||||
if (oldVersion < 28 && newVersion >= 28) {
|
||||
canonicalizeJids(db);
|
||||
}
|
||||
|
||||
if (oldVersion < 29 && newVersion >= 29) {
|
||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT");
|
||||
}
|
||||
if (oldVersion < 30 && newVersion >= 30) {
|
||||
db.execSQL(CREATE_START_TIMES_TABLE);
|
||||
}
|
||||
if (oldVersion < 31 && newVersion >= 31) {
|
||||
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.TRUST + " TEXT");
|
||||
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.ACTIVE + " NUMBER");
|
||||
HashMap<Integer,ContentValues> migration = new HashMap<>();
|
||||
migration.put(0,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED,true));
|
||||
migration.put(1,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
|
||||
migration.put(2,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true));
|
||||
migration.put(3,createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false));
|
||||
migration.put(4,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
|
||||
migration.put(5,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
|
||||
migration.put(6,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false));
|
||||
migration.put(7,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true));
|
||||
migration.put(8,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false));
|
||||
for(Map.Entry<Integer,ContentValues> entry : migration.entrySet()) {
|
||||
String whereClause = SQLiteAxolotlStore.TRUSTED+"=?";
|
||||
String[] where = {String.valueOf(entry.getKey())};
|
||||
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,entry.getValue(),whereClause,where);
|
||||
}
|
||||
|
||||
}
|
||||
if (oldVersion < 32 && newVersion >= 32) {
|
||||
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER");
|
||||
ContentValues defaults = new ContentValues();
|
||||
defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION,System.currentTimeMillis());
|
||||
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,defaults,null,null);
|
||||
}
|
||||
if (oldVersion < 33 && newVersion >= 33) {
|
||||
String whereClause = SQLiteAxolotlStore.OWN+"=1";
|
||||
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED,true),whereClause,null);
|
||||
}
|
||||
|
||||
if (oldVersion < 34 && newVersion >= 34) {
|
||||
db.execSQL(CREATE_MESSAGE_TIME_INDEX);
|
||||
|
||||
final File oldPicturesDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)+"/Conversations/");
|
||||
final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/");
|
||||
final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Files/");
|
||||
final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Videos/");
|
||||
if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) {
|
||||
final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Images/");
|
||||
newPicturesDirectory.getParentFile().mkdirs();
|
||||
if (oldPicturesDirectory.renameTo(newPicturesDirectory)) {
|
||||
Log.d(Config.LOGTAG,"moved "+oldPicturesDirectory.getAbsolutePath()+" to "+newPicturesDirectory.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) {
|
||||
newFilesDirectory.mkdirs();
|
||||
newVideosDirectory.mkdirs();
|
||||
for(File file : oldFilesDirectory.listFiles()) {
|
||||
if (file.getName().equals(".nomedia")) {
|
||||
if (file.delete()) {
|
||||
Log.d(Config.LOGTAG,"deleted nomedia file in "+oldFilesDirectory.getAbsolutePath());
|
||||
}
|
||||
} else if (file.isFile()) {
|
||||
final String name = file.getName();
|
||||
boolean isVideo = false;
|
||||
int start = name.lastIndexOf('.') + 1;
|
||||
if (start < name.length()) {
|
||||
String mime= MimeUtils.guessMimeTypeFromExtension(name.substring(start));
|
||||
isVideo = mime != null && mime.startsWith("video/");
|
||||
}
|
||||
File dst = new File((isVideo ? newVideosDirectory : newFilesDirectory).getAbsolutePath()+"/"+file.getName());
|
||||
if (file.renameTo(dst)) {
|
||||
Log.d(Config.LOGTAG, "moved " + file + " to " + dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(SQLiteAxolotlStore.TRUST,trust.toString());
|
||||
values.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0);
|
||||
return values;
|
||||
}
|
||||
|
||||
private void canonicalizeJids(SQLiteDatabase db) {
|
||||
// migrate db to new, canonicalized JID domainpart representation
|
||||
|
||||
// Conversation table
|
||||
Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]);
|
||||
while (cursor.moveToNext()) {
|
||||
String newJid;
|
||||
try {
|
||||
newJid = Jid.fromString(
|
||||
cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
|
||||
).toPreppedString();
|
||||
} catch (InvalidJidException ignored) {
|
||||
Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID "
|
||||
+ cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
|
||||
+ ": " + ignored + ". Skipping...");
|
||||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
newJid,
|
||||
cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
|
||||
};
|
||||
db.execSQL("update " + Conversation.TABLENAME
|
||||
+ " set " + Conversation.CONTACTJID + " = ? "
|
||||
+ " where " + Conversation.UUID + " = ?", updateArgs);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
// Contact table
|
||||
cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]);
|
||||
while (cursor.moveToNext()) {
|
||||
String newJid;
|
||||
try {
|
||||
newJid = Jid.fromString(
|
||||
cursor.getString(cursor.getColumnIndex(Contact.JID))
|
||||
).toPreppedString();
|
||||
} catch (InvalidJidException ignored) {
|
||||
Log.e(Config.LOGTAG, "Failed to migrate Contact JID "
|
||||
+ cursor.getString(cursor.getColumnIndex(Contact.JID))
|
||||
+ ": " + ignored + ". Skipping...");
|
||||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
newJid,
|
||||
cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
|
||||
cursor.getString(cursor.getColumnIndex(Contact.JID)),
|
||||
};
|
||||
db.execSQL("update " + Contact.TABLENAME
|
||||
+ " set " + Contact.JID + " = ? "
|
||||
+ " where " + Contact.ACCOUNT + " = ? "
|
||||
+ " AND " + Contact.JID + " = ?", updateArgs);
|
||||
}
|
||||
cursor.close();
|
||||
|
||||
// Account table
|
||||
cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]);
|
||||
while (cursor.moveToNext()) {
|
||||
String newServer;
|
||||
try {
|
||||
newServer = Jid.fromParts(
|
||||
cursor.getString(cursor.getColumnIndex(Account.USERNAME)),
|
||||
cursor.getString(cursor.getColumnIndex(Account.SERVER)),
|
||||
"mobile"
|
||||
).getDomainpart();
|
||||
} catch (InvalidJidException ignored) {
|
||||
Log.e(Config.LOGTAG, "Failed to migrate Account SERVER "
|
||||
+ cursor.getString(cursor.getColumnIndex(Account.SERVER))
|
||||
+ ": " + ignored + ". Skipping...");
|
||||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
newServer,
|
||||
cursor.getString(cursor.getColumnIndex(Account.UUID)),
|
||||
};
|
||||
db.execSQL("update " + Account.TABLENAME
|
||||
+ " set " + Account.SERVER + " = ? "
|
||||
+ " where " + Account.UUID + " = ?", updateArgs);
|
||||
}
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
public static synchronized DatabaseBackend getInstance(Context context) {
|
||||
|
@ -379,6 +546,56 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.insert(Account.TABLENAME, null, account.getContentValues());
|
||||
}
|
||||
|
||||
public void insertDiscoveryResult(ServiceDiscoveryResult result) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues());
|
||||
}
|
||||
|
||||
public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) {
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
String[] selectionArgs = {hash, ver};
|
||||
Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null,
|
||||
ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?",
|
||||
selectionArgs, null, null, null);
|
||||
if (cursor.getCount() == 0) {
|
||||
cursor.close();
|
||||
return null;
|
||||
}
|
||||
cursor.moveToFirst();
|
||||
|
||||
ServiceDiscoveryResult result = null;
|
||||
try {
|
||||
result = new ServiceDiscoveryResult(cursor);
|
||||
} catch (JSONException e) { /* result is still null */ }
|
||||
|
||||
cursor.close();
|
||||
return result;
|
||||
}
|
||||
|
||||
public void insertPresenceTemplate(PresenceTemplate template) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues());
|
||||
}
|
||||
|
||||
public List<PresenceTemplate> getPresenceTemplates() {
|
||||
ArrayList<PresenceTemplate> templates = new ArrayList<>();
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
Cursor cursor = db.query(PresenceTemplate.TABELNAME,null,null,null,null,null,PresenceTemplate.LAST_USED+" desc");
|
||||
while (cursor.moveToNext()) {
|
||||
templates.add(PresenceTemplate.fromCursor(cursor));
|
||||
}
|
||||
cursor.close();
|
||||
return templates;
|
||||
}
|
||||
|
||||
public void deletePresenceTemplate(PresenceTemplate template) {
|
||||
Log.d(Config.LOGTAG,"deleting presence template with uuid "+template.getUuid());
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
String where = PresenceTemplate.UUID+"=?";
|
||||
String[] whereArgs = {template.getUuid()};
|
||||
db.delete(PresenceTemplate.TABELNAME,where,whereArgs);
|
||||
}
|
||||
|
||||
public CopyOnWriteArrayList<Conversation> getConversations(int status) {
|
||||
CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>();
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
|
@ -467,14 +684,16 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
public Conversation findConversation(final Account account, final Jid contactJid) {
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
String[] selectionArgs = {account.getUuid(),
|
||||
contactJid.toBareJid().toString() + "/%",
|
||||
contactJid.toBareJid().toString()
|
||||
contactJid.toBareJid().toPreppedString() + "/%",
|
||||
contactJid.toBareJid().toPreppedString()
|
||||
};
|
||||
Cursor cursor = db.query(Conversation.TABLENAME, null,
|
||||
Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
|
||||
+ " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
|
||||
if (cursor.getCount() == 0)
|
||||
if (cursor.getCount() == 0) {
|
||||
cursor.close();
|
||||
return null;
|
||||
}
|
||||
cursor.moveToFirst();
|
||||
Conversation conversation = Conversation.fromCursor(cursor);
|
||||
cursor.close();
|
||||
|
@ -504,17 +723,18 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
return list;
|
||||
}
|
||||
|
||||
public void updateAccount(Account account) {
|
||||
public boolean updateAccount(Account account) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
String[] args = {account.getUuid()};
|
||||
db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
|
||||
+ "=?", args);
|
||||
final int rows = db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args);
|
||||
return rows == 1;
|
||||
}
|
||||
|
||||
public void deleteAccount(Account account) {
|
||||
public boolean deleteAccount(Account account) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
String[] args = {account.getUuid()};
|
||||
db.delete(Account.TABLENAME, Account.UUID + "=?", args);
|
||||
final int rows = db.delete(Account.TABLENAME, Account.UUID + "=?", args);
|
||||
return rows == 1;
|
||||
}
|
||||
|
||||
public boolean hasEnabledAccounts() {
|
||||
|
@ -524,12 +744,15 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
try {
|
||||
cursor.moveToFirst();
|
||||
int count = cursor.getInt(0);
|
||||
cursor.close();
|
||||
return (count > 0);
|
||||
} catch (SQLiteCantOpenDatabaseException e) {
|
||||
return true; // better safe than sorry
|
||||
} catch (RuntimeException e) {
|
||||
return true; // better safe than sorry
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -547,6 +770,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ "=?", args);
|
||||
}
|
||||
|
||||
public void updateMessage(Message message, String uuid) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
String[] args = {uuid};
|
||||
db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
|
||||
+ "=?", args);
|
||||
}
|
||||
|
||||
public void readRoster(Roster roster) {
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
Cursor cursor;
|
||||
|
@ -567,7 +797,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.insert(Contact.TABLENAME, null, contact.getContentValues());
|
||||
} else {
|
||||
String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?";
|
||||
String[] whereArgs = {account.getUuid(), contact.getJid().toString()};
|
||||
String[] whereArgs = {account.getUuid(), contact.getJid().toPreppedString()};
|
||||
db.delete(Contact.TABLENAME, where, whereArgs);
|
||||
}
|
||||
}
|
||||
|
@ -583,12 +813,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
|
||||
}
|
||||
|
||||
public boolean expireOldMessages(long timestamp) {
|
||||
String where = Message.TIME_SENT+"<?";
|
||||
String[] whereArgs = {String.valueOf(timestamp)};
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
return db.delete(Message.TABLENAME,where,whereArgs) > 0;
|
||||
}
|
||||
|
||||
public Pair<Long, String> getLastMessageReceived(Account account) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1";
|
||||
String[] args = {account.getUuid()};
|
||||
Cursor cursor = db.rawQuery(sql, args);
|
||||
cursor = db.rawQuery(sql, args);
|
||||
if (cursor.getCount() == 0) {
|
||||
return null;
|
||||
} else {
|
||||
|
@ -597,24 +835,58 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public long getLastTimeFingerprintUsed(Account account, String fingerprint) {
|
||||
String SQL = "select messages.timeSent from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and messages.axolotl_fingerprint=? order by messages.timesent desc limit 1";
|
||||
String[] args = {account.getUuid(), fingerprint};
|
||||
Cursor cursor = getReadableDatabase().rawQuery(SQL,args);
|
||||
long time;
|
||||
if (cursor.moveToFirst()) {
|
||||
time = cursor.getLong(0);
|
||||
} else {
|
||||
time = 0;
|
||||
}
|
||||
cursor.close();
|
||||
return time;
|
||||
}
|
||||
|
||||
public Pair<Long,String> getLastClearDate(Account account) {
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
String[] columns = {Conversation.ATTRIBUTES};
|
||||
String selection = Conversation.ACCOUNT + "=?";
|
||||
String[] args = {account.getUuid()};
|
||||
Cursor cursor = db.query(Conversation.TABLENAME,columns,selection,args,null,null,null);
|
||||
long maxClearDate = 0;
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
final JSONObject jsonObject = new JSONObject(cursor.getString(0));
|
||||
maxClearDate = Math.max(maxClearDate, jsonObject.getLong(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY));
|
||||
} catch (Exception e) {
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
cursor.close();
|
||||
return new Pair<>(maxClearDate,null);
|
||||
}
|
||||
|
||||
private Cursor getCursorForSession(Account account, AxolotlAddress contact) {
|
||||
final SQLiteDatabase db = this.getReadableDatabase();
|
||||
String[] columns = null;
|
||||
String[] selectionArgs = {account.getUuid(),
|
||||
contact.getName(),
|
||||
Integer.toString(contact.getDeviceId())};
|
||||
Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
|
||||
columns,
|
||||
return db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
|
||||
null,
|
||||
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
|
||||
+ SQLiteAxolotlStore.NAME + " = ? AND "
|
||||
+ SQLiteAxolotlStore.DEVICE_ID + " = ? ",
|
||||
selectionArgs,
|
||||
null, null, null);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public SessionRecord loadSession(Account account, AxolotlAddress contact) {
|
||||
|
@ -848,7 +1120,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
|
||||
private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) {
|
||||
String[] columns = {SQLiteAxolotlStore.TRUSTED,
|
||||
String[] columns = {SQLiteAxolotlStore.TRUST,
|
||||
SQLiteAxolotlStore.ACTIVE,
|
||||
SQLiteAxolotlStore.LAST_ACTIVATION,
|
||||
SQLiteAxolotlStore.KEY};
|
||||
ArrayList<String> selectionArgs = new ArrayList<>(4);
|
||||
selectionArgs.add(account.getUuid());
|
||||
|
@ -880,7 +1154,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
|
||||
private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) {
|
||||
String name = account.getJid().toBareJid().toString();
|
||||
String name = account.getJid().toBareJid().toPreppedString();
|
||||
IdentityKeyPair identityKeyPair = null;
|
||||
Cursor cursor = getIdentityKeyCursor(db, account, name, true);
|
||||
if (cursor.getCount() != 0) {
|
||||
|
@ -900,18 +1174,21 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
return loadIdentityKeys(account, name, null);
|
||||
}
|
||||
|
||||
public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
|
||||
public Set<IdentityKey> loadIdentityKeys(Account account, String name, FingerprintStatus status) {
|
||||
Set<IdentityKey> identityKeys = new HashSet<>();
|
||||
Cursor cursor = getIdentityKeyCursor(account, name, false);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
if (trust != null &&
|
||||
cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED))
|
||||
!= trust.getCode()) {
|
||||
if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT), 0));
|
||||
String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY));
|
||||
if (key != null) {
|
||||
identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0));
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name);
|
||||
}
|
||||
} catch (InvalidKeyException e) {
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
|
||||
}
|
||||
|
@ -926,22 +1203,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
String[] args = {
|
||||
account.getUuid(),
|
||||
name,
|
||||
String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode()),
|
||||
String.valueOf(XmppAxolotlSession.Trust.TRUSTED_X509.getCode())
|
||||
FingerprintStatus.Trust.TRUSTED.toString(),
|
||||
FingerprintStatus.Trust.VERIFIED.toString(),
|
||||
FingerprintStatus.Trust.VERIFIED_X509.toString()
|
||||
};
|
||||
return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
|
||||
SQLiteAxolotlStore.ACCOUNT + " = ?"
|
||||
+ " AND " + SQLiteAxolotlStore.NAME + " = ?"
|
||||
+ " AND (" + SQLiteAxolotlStore.TRUSTED + " = ? OR " + SQLiteAxolotlStore.TRUSTED + " = ?)",
|
||||
+ " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " +SQLiteAxolotlStore.TRUST +" = ?)"
|
||||
+ " AND " +SQLiteAxolotlStore.ACTIVE + " > 0",
|
||||
args
|
||||
);
|
||||
}
|
||||
|
||||
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
|
||||
storeIdentityKey(account, name, own, fingerprint, base64Serialized, XmppAxolotlSession.Trust.UNDECIDED);
|
||||
}
|
||||
|
||||
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, XmppAxolotlSession.Trust trusted) {
|
||||
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
|
||||
|
@ -949,35 +1224,50 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
|
||||
values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
|
||||
values.put(SQLiteAxolotlStore.KEY, base64Serialized);
|
||||
values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode());
|
||||
values.putAll(status.toContentValues());
|
||||
String where = SQLiteAxolotlStore.ACCOUNT+"=? AND "+SQLiteAxolotlStore.NAME+"=? AND "+SQLiteAxolotlStore.FINGERPRINT+" =?";
|
||||
String[] whereArgs = {account.getUuid(),name,fingerprint};
|
||||
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,values,where,whereArgs);
|
||||
if (rows == 0) {
|
||||
db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
|
||||
}
|
||||
}
|
||||
|
||||
public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
|
||||
values.put(SQLiteAxolotlStore.NAME, name);
|
||||
values.put(SQLiteAxolotlStore.OWN, 0);
|
||||
values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
|
||||
values.putAll(status.toContentValues());
|
||||
db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
|
||||
}
|
||||
|
||||
public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
|
||||
public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) {
|
||||
Cursor cursor = getIdentityKeyCursor(account, fingerprint);
|
||||
XmppAxolotlSession.Trust trust = null;
|
||||
final FingerprintStatus status;
|
||||
if (cursor.getCount() > 0) {
|
||||
cursor.moveToFirst();
|
||||
int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED));
|
||||
trust = XmppAxolotlSession.Trust.fromCode(trustValue);
|
||||
status = FingerprintStatus.fromCursor(cursor);
|
||||
} else {
|
||||
status = null;
|
||||
}
|
||||
cursor.close();
|
||||
return trust;
|
||||
return status;
|
||||
}
|
||||
|
||||
public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
|
||||
public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
return setIdentityKeyTrust(db, account, fingerprint, trust);
|
||||
return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus);
|
||||
}
|
||||
|
||||
private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
|
||||
private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) {
|
||||
String[] selectionArgs = {
|
||||
account.getUuid(),
|
||||
fingerprint
|
||||
};
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode());
|
||||
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
|
||||
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(),
|
||||
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
|
||||
+ SQLiteAxolotlStore.FINGERPRINT + " = ? ",
|
||||
selectionArgs);
|
||||
|
@ -1017,6 +1307,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
} else {
|
||||
cursor.moveToFirst();
|
||||
byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE));
|
||||
cursor.close();
|
||||
if (certificate == null || certificate.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1030,17 +1321,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
|
||||
storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
|
||||
public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) {
|
||||
storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status);
|
||||
}
|
||||
|
||||
public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {
|
||||
storeIdentityKey(account, account.getJid().toBareJid().toString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED);
|
||||
storeIdentityKey(account, account.getJid().toBareJid().toPreppedString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false));
|
||||
}
|
||||
|
||||
public void recreateAxolotlDb() {
|
||||
recreateAxolotlDb(getWritableDatabase());
|
||||
}
|
||||
|
||||
public void recreateAxolotlDb(SQLiteDatabase db) {
|
||||
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<");
|
||||
|
@ -1074,4 +1362,35 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
SQLiteAxolotlStore.ACCOUNT + " = ?",
|
||||
deleteArgs);
|
||||
}
|
||||
|
||||
public boolean startTimeCountExceedsThreshold() {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
long cleanBeforeTimestamp = System.currentTimeMillis() - Config.FREQUENT_RESTARTS_DETECTION_WINDOW;
|
||||
db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp < "+cleanBeforeTimestamp);
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("timestamp",System.currentTimeMillis());
|
||||
db.insert(START_TIMES_TABLE,null,values);
|
||||
String[] columns = new String[]{"count(timestamp)"};
|
||||
Cursor cursor = db.query(START_TIMES_TABLE,columns,null,null,null,null,null);
|
||||
int count;
|
||||
if (cursor.moveToFirst()) {
|
||||
count = cursor.getInt(0);
|
||||
} else {
|
||||
count = 0;
|
||||
}
|
||||
cursor.close();
|
||||
Log.d(Config.LOGTAG,"start time counter reached "+count);
|
||||
return count >= Config.FREQUENT_RESTARTS_THRESHOLD;
|
||||
}
|
||||
|
||||
public void clearStartTimeCounter(boolean justOne) {
|
||||
SQLiteDatabase db = this.getWritableDatabase();
|
||||
if (justOne) {
|
||||
db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp in (select timestamp from "+START_TIMES_TABLE+" order by timestamp desc limit 1)");
|
||||
Log.d(Config.LOGTAG,"do not count start up after being swiped away");
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,"resetting start time counter");
|
||||
db.execSQL("delete from " + START_TIMES_TABLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,20 +1,37 @@
|
|||
package eu.siacs.conversations.persistance;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.v4.content.FileProvider;
|
||||
import android.system.Os;
|
||||
import android.system.StructStat;
|
||||
import android.util.Base64;
|
||||
import android.util.Base64OutputStream;
|
||||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -26,23 +43,26 @@ import java.security.DigestOutputStream;
|
|||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.DownloadableFile;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.ExifHelper;
|
||||
import eu.siacs.conversations.utils.FileUtils;
|
||||
import eu.siacs.conversations.utils.FileWriterException;
|
||||
import eu.siacs.conversations.utils.MimeUtils;
|
||||
import eu.siacs.conversations.xmpp.pep.Avatar;
|
||||
|
||||
public class FileBackend {
|
||||
private final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
|
||||
private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
|
||||
|
||||
private static final String FILE_PROVIDER = ".files";
|
||||
|
||||
private XmppConnectionService mXmppConnectionService;
|
||||
|
||||
|
@ -50,6 +70,38 @@ public class FileBackend {
|
|||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
private void createNoMedia() {
|
||||
final File nomedia = new File(getConversationsDirectory("Files")+".nomedia");
|
||||
if (!nomedia.exists()) {
|
||||
try {
|
||||
nomedia.createNewFile();
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "could not create nomedia file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMediaScanner(File file) {
|
||||
String path = file.getAbsolutePath();
|
||||
if (!path.startsWith(getConversationsDirectory("Files"))) {
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||
intent.setData(Uri.fromFile(file));
|
||||
mXmppConnectionService.sendBroadcast(intent);
|
||||
} else {
|
||||
createNoMedia();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean deleteFile(Message message) {
|
||||
File file = getFile(message);
|
||||
if (file.delete()) {
|
||||
updateMediaScanner(file);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public DownloadableFile getFile(Message message) {
|
||||
return getFile(message, true);
|
||||
}
|
||||
|
@ -67,27 +119,54 @@ public class FileBackend {
|
|||
file = new DownloadableFile(path);
|
||||
} else {
|
||||
String mime = message.getMimeType();
|
||||
if (mime != null && mime.startsWith("image")) {
|
||||
file = new DownloadableFile(getConversationsImageDirectory() + path);
|
||||
if (mime != null && mime.startsWith("image/")) {
|
||||
file = new DownloadableFile(getConversationsDirectory("Images") + path);
|
||||
} else if (mime != null && mime.startsWith("video/")) {
|
||||
file = new DownloadableFile(getConversationsDirectory("Videos") + path);
|
||||
} else {
|
||||
file = new DownloadableFile(getConversationsFileDirectory() + path);
|
||||
file = new DownloadableFile(getConversationsDirectory("Files") + path);
|
||||
}
|
||||
}
|
||||
if (encrypted) {
|
||||
return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp");
|
||||
return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp");
|
||||
} else {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getConversationsFileDirectory() {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
|
||||
private static long getFileSize(Context context, Uri uri) {
|
||||
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getConversationsImageDirectory() {
|
||||
return Environment.getExternalStoragePublicDirectory(
|
||||
Environment.DIRECTORY_PICTURES).getAbsolutePath()
|
||||
+ "/Conversations/";
|
||||
public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) {
|
||||
if (max <= 0) {
|
||||
Log.d(Config.LOGTAG,"server did not report max file size for http upload");
|
||||
return true; //exception to be compatible with HTTP Upload < v0.2
|
||||
}
|
||||
for(Uri uri : uris) {
|
||||
if (FileBackend.getFileSize(context, uri) > max) {
|
||||
Log.d(Config.LOGTAG,"not all files are under "+max+" bytes. suggesting falling back to jingle");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getConversationsDirectory(final String type) {
|
||||
if (Config.ONLY_INTERNAL_STORAGE) {
|
||||
return mXmppConnectionService.getFilesDir().getAbsolutePath()+"/"+type+"/";
|
||||
} else {
|
||||
return Environment.getExternalStorageDirectory() +"/Conversations/Media/Conversations "+type+"/";
|
||||
}
|
||||
}
|
||||
|
||||
public static String getConversationsLogsDirectory() {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
|
||||
}
|
||||
|
||||
public Bitmap resize(Bitmap originalBitmap, int size) {
|
||||
|
@ -156,6 +235,7 @@ public class FileBackend {
|
|||
}
|
||||
|
||||
public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
|
||||
Log.d(Config.LOGTAG,"copy file ("+uri.toString()+") to private storage "+file.getAbsolutePath());
|
||||
file.getParentFile().mkdirs();
|
||||
OutputStream os = null;
|
||||
InputStream is = null;
|
||||
|
@ -166,11 +246,21 @@ public class FileBackend {
|
|||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, length);
|
||||
try {
|
||||
os.write(buffer, 0, length);
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
}
|
||||
try {
|
||||
os.flush();
|
||||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
os.flush();
|
||||
} catch(FileNotFoundException e) {
|
||||
throw new FileCopyException(R.string.error_file_not_found);
|
||||
} catch(FileWriterException e) {
|
||||
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new FileCopyException(R.string.error_io_exception);
|
||||
|
@ -178,24 +268,50 @@ public class FileBackend {
|
|||
close(os);
|
||||
close(is);
|
||||
}
|
||||
Log.d(Config.LOGTAG, "output file name " + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
|
||||
Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
|
||||
String mime = mXmppConnectionService.getContentResolver().getType(uri);
|
||||
String mime = MimeUtils.guessMimeTypeFromUri(mXmppConnectionService, uri);
|
||||
Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime="+mime+")");
|
||||
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
|
||||
if (extension == null) {
|
||||
extension = getExtensionFromUri(uri);
|
||||
}
|
||||
message.setRelativeFilePath(message.getUuid() + "." + extension);
|
||||
copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri);
|
||||
}
|
||||
|
||||
private String getExtensionFromUri(Uri uri) {
|
||||
String[] projection = {MediaStore.MediaColumns.DATA};
|
||||
String filename = null;
|
||||
Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null);
|
||||
if (cursor != null) {
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
filename = cursor.getString(0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
filename = null;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
int pos = filename == null ? -1 : filename.lastIndexOf('.');
|
||||
return pos > 0 ? filename.substring(pos+1) : null;
|
||||
}
|
||||
|
||||
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException {
|
||||
file.getParentFile().mkdirs();
|
||||
InputStream is = null;
|
||||
OutputStream os = null;
|
||||
try {
|
||||
file.createNewFile();
|
||||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
|
||||
}
|
||||
is = mXmppConnectionService.getContentResolver().openInputStream(image);
|
||||
if (is == null) {
|
||||
throw new FileCopyException(R.string.error_not_an_image_file);
|
||||
}
|
||||
Bitmap originalBitmap;
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
int inSampleSize = (int) Math.pow(2, sampleSize);
|
||||
|
@ -222,7 +338,6 @@ public class FileBackend {
|
|||
quality -= 5;
|
||||
}
|
||||
scaledBitmap.recycle();
|
||||
return;
|
||||
} catch (FileNotFoundException e) {
|
||||
throw new FileCopyException(R.string.error_file_not_found);
|
||||
} catch (IOException e) {
|
||||
|
@ -237,8 +352,6 @@ public class FileBackend {
|
|||
} else {
|
||||
throw new FileCopyException(R.string.error_out_of_memory);
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
throw new FileCopyException(R.string.error_io_exception);
|
||||
} finally {
|
||||
close(os);
|
||||
close(is);
|
||||
|
@ -246,6 +359,7 @@ public class FileBackend {
|
|||
}
|
||||
|
||||
public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException {
|
||||
Log.d(Config.LOGTAG,"copy image ("+image.toString()+") to private storage "+file.getAbsolutePath());
|
||||
copyImageToPrivateStorage(file, image, 0);
|
||||
}
|
||||
|
||||
|
@ -281,35 +395,121 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
|
||||
throws FileNotFoundException {
|
||||
Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(message.getUuid());
|
||||
public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException {
|
||||
final String uuid = message.getUuid();
|
||||
final LruCache<String,Bitmap> cache = mXmppConnectionService.getBitmapCache();
|
||||
Bitmap thumbnail = cache.get(uuid);
|
||||
if ((thumbnail == null) && (!cacheOnly)) {
|
||||
File file = getFile(message);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = calcSampleSize(file, size);
|
||||
Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options);
|
||||
if (fullsize == null) {
|
||||
throw new FileNotFoundException();
|
||||
synchronized (cache) {
|
||||
thumbnail = cache.get(uuid);
|
||||
if (thumbnail != null) {
|
||||
return thumbnail;
|
||||
}
|
||||
DownloadableFile file = getFile(message);
|
||||
final String mime = file.getMimeType();
|
||||
if (mime.startsWith("video/")) {
|
||||
thumbnail = getVideoPreview(file, size);
|
||||
} else {
|
||||
Bitmap fullsize = getFullsizeImagePreview(file, size);
|
||||
if (fullsize == null) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
thumbnail = resize(fullsize, size);
|
||||
thumbnail = rotate(thumbnail, getRotation(file));
|
||||
if (mime.equals("image/gif")) {
|
||||
Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888,true);
|
||||
drawOverlay(withGifOverlay,R.drawable.play_gif,1.0f);
|
||||
thumbnail.recycle();
|
||||
thumbnail = withGifOverlay;
|
||||
}
|
||||
}
|
||||
this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail);
|
||||
}
|
||||
thumbnail = resize(fullsize, size);
|
||||
thumbnail = rotate(thumbnail, getRotation(file));
|
||||
this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),thumbnail);
|
||||
}
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
private Bitmap getFullsizeImagePreview(File file, int size) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = calcSampleSize(file, size);
|
||||
try {
|
||||
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
} catch (OutOfMemoryError e) {
|
||||
options.inSampleSize *= 2;
|
||||
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawOverlay(Bitmap bitmap, int resource, float factor) {
|
||||
Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
paint.setFilterBitmap(true);
|
||||
paint.setDither(true);
|
||||
float targetSize = Math.min(canvas.getWidth(),canvas.getHeight()) * factor;
|
||||
Log.d(Config.LOGTAG,"target size overlay: "+targetSize+" overlay bitmap size was "+overlay.getHeight());
|
||||
float left = (canvas.getWidth() - targetSize) / 2.0f;
|
||||
float top = (canvas.getHeight() - targetSize) / 2.0f;
|
||||
RectF dst = new RectF(left,top,left+targetSize-1,top+targetSize-1);
|
||||
canvas.drawBitmap(overlay,null,dst,paint);
|
||||
}
|
||||
|
||||
private Bitmap getVideoPreview(File file, int size) {
|
||||
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
|
||||
Bitmap frame;
|
||||
try {
|
||||
metadataRetriever.setDataSource(file.getAbsolutePath());
|
||||
frame = metadataRetriever.getFrameAtTime(0);
|
||||
metadataRetriever.release();
|
||||
frame = resize(frame, size);
|
||||
} catch(IllegalArgumentException | NullPointerException e) {
|
||||
frame = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888);
|
||||
frame.eraseColor(0xff000000);
|
||||
}
|
||||
drawOverlay(frame,R.drawable.play_video,0.75f);
|
||||
return frame;
|
||||
}
|
||||
|
||||
private static String getTakePhotoPath() {
|
||||
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)+"/Camera/";
|
||||
}
|
||||
|
||||
public Uri getTakePhotoUri() {
|
||||
StringBuilder pathBuilder = new StringBuilder();
|
||||
pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
|
||||
pathBuilder.append('/');
|
||||
pathBuilder.append("Camera");
|
||||
pathBuilder.append('/');
|
||||
pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + ".jpg");
|
||||
Uri uri = Uri.parse("file://" + pathBuilder.toString());
|
||||
File file = new File(uri.toString());
|
||||
File file;
|
||||
if (Config.ONLY_INTERNAL_STORAGE) {
|
||||
file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath(), "Camera/IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
|
||||
} else {
|
||||
file = new File(getTakePhotoPath() + "IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
|
||||
}
|
||||
file.getParentFile().mkdirs();
|
||||
return uri;
|
||||
return getUriForFile(mXmppConnectionService,file);
|
||||
}
|
||||
|
||||
public static Uri getUriForFile(Context context, File file) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
|
||||
try {
|
||||
String packageId = context.getPackageName();
|
||||
return FileProvider.getUriForFile(context, packageId + FILE_PROVIDER, file);
|
||||
} catch(IllegalArgumentException e) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
throw new SecurityException();
|
||||
} else {
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
public static Uri getIndexableTakePhotoUri(Uri original) {
|
||||
if (Config.ONLY_INTERNAL_STORAGE || "file".equals(original.getScheme())) {
|
||||
return original;
|
||||
} else {
|
||||
List<String> segments = original.getPathSegments();
|
||||
return Uri.parse("file://"+getTakePhotoPath()+segments.get(segments.size() - 1));
|
||||
}
|
||||
}
|
||||
|
||||
public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
|
||||
|
@ -320,11 +520,11 @@ public class FileBackend {
|
|||
return null;
|
||||
}
|
||||
ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
|
||||
Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
|
||||
Base64OutputStream mBase64OutputStream = new Base64OutputStream(
|
||||
mByteArrayOutputStream, Base64.DEFAULT);
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
DigestOutputStream mDigestOutputStream = new DigestOutputStream(
|
||||
mBase64OutputSttream, digest);
|
||||
mBase64OutputStream, digest);
|
||||
if (!bm.compress(format, 75, mDigestOutputStream)) {
|
||||
return null;
|
||||
}
|
||||
|
@ -340,6 +540,45 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
public Avatar getStoredPepAvatar(String hash) {
|
||||
if (hash == null) {
|
||||
return null;
|
||||
}
|
||||
Avatar avatar = new Avatar();
|
||||
File file = new File(getAvatarPath(hash));
|
||||
FileInputStream is = null;
|
||||
try {
|
||||
avatar.size = file.length();
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
is = new FileInputStream(file);
|
||||
ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
|
||||
Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest);
|
||||
byte[] buffer = new byte[4096];
|
||||
int length;
|
||||
while ((length = is.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, length);
|
||||
}
|
||||
os.flush();
|
||||
os.close();
|
||||
avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
|
||||
avatar.image = new String(mByteArrayOutputStream.toByteArray());
|
||||
avatar.height = options.outHeight;
|
||||
avatar.width = options.outWidth;
|
||||
avatar.type = options.outMimeType;
|
||||
return avatar;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
} finally {
|
||||
close(is);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAvatarCached(Avatar avatar) {
|
||||
File file = new File(getAvatarPath(avatar.getFilename()));
|
||||
return file.exists();
|
||||
|
@ -349,6 +588,7 @@ public class FileBackend {
|
|||
File file;
|
||||
if (isAvatarCached(avatar)) {
|
||||
file = new File(getAvatarPath(avatar.getFilename()));
|
||||
avatar.size = file.length();
|
||||
} else {
|
||||
String filename = getAvatarPath(avatar.getFilename());
|
||||
file = new File(filename + ".tmp");
|
||||
|
@ -360,7 +600,8 @@ public class FileBackend {
|
|||
MessageDigest digest = MessageDigest.getInstance("SHA-1");
|
||||
digest.reset();
|
||||
DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
|
||||
mDigestOutputStream.write(avatar.getImageAsBytes());
|
||||
final byte[] bytes = avatar.getImageAsBytes();
|
||||
mDigestOutputStream.write(bytes);
|
||||
mDigestOutputStream.flush();
|
||||
mDigestOutputStream.close();
|
||||
String sha1sum = CryptoHelper.bytesToHex(digest.digest());
|
||||
|
@ -371,13 +612,13 @@ public class FileBackend {
|
|||
file.delete();
|
||||
return false;
|
||||
}
|
||||
avatar.size = bytes.length;
|
||||
} catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
|
||||
return false;
|
||||
} finally {
|
||||
close(os);
|
||||
}
|
||||
}
|
||||
avatar.size = file.length();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -447,7 +688,7 @@ public class FileBackend {
|
|||
Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(dest);
|
||||
canvas.drawBitmap(source, null, targetRect, null);
|
||||
if (source != null && !source.isRecycled()) {
|
||||
if (source.isRecycled()) {
|
||||
source.recycle();
|
||||
}
|
||||
return dest;
|
||||
|
@ -512,37 +753,106 @@ public class FileBackend {
|
|||
return inSampleSize;
|
||||
}
|
||||
|
||||
public Uri getJingleFileUri(Message message) {
|
||||
File file = getFile(message);
|
||||
return Uri.parse("file://" + file.getAbsolutePath());
|
||||
}
|
||||
|
||||
public void updateFileParams(Message message) {
|
||||
updateFileParams(message,null);
|
||||
}
|
||||
|
||||
public void updateFileParams(Message message, URL url) {
|
||||
DownloadableFile file = getFile(message);
|
||||
if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
int rotation = getRotation(file);
|
||||
boolean rotated = rotation == 90 || rotation == 270;
|
||||
int imageHeight = rotated ? options.outWidth : options.outHeight;
|
||||
int imageWidth = rotated ? options.outHeight : options.outWidth;
|
||||
if (url == null) {
|
||||
message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
|
||||
} else {
|
||||
message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
|
||||
}
|
||||
} else {
|
||||
if (url != null) {
|
||||
message.setBody(url.toString()+"|"+Long.toString(file.getSize()));
|
||||
} else {
|
||||
message.setBody(Long.toString(file.getSize()));
|
||||
final String mime = file.getMimeType();
|
||||
boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/"));
|
||||
boolean video = mime != null && mime.startsWith("video/");
|
||||
if (image || video) {
|
||||
try {
|
||||
Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file);
|
||||
if (url == null) {
|
||||
message.setBody(Long.toString(file.getSize()) + '|' + dimensions.width + '|' + dimensions.height);
|
||||
} else {
|
||||
message.setBody(url.toString() + "|" + Long.toString(file.getSize()) + '|' + dimensions.width + '|' + dimensions.height);
|
||||
}
|
||||
return;
|
||||
} catch (NotAVideoFile notAVideoFile) {
|
||||
Log.d(Config.LOGTAG,"file with mime type "+file.getMimeType()+" was not a video file");
|
||||
//fall threw
|
||||
}
|
||||
}
|
||||
if (url != null) {
|
||||
message.setBody(url.toString()+"|"+Long.toString(file.getSize()));
|
||||
} else {
|
||||
message.setBody(Long.toString(file.getSize()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Dimensions getImageDimensions(File file) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
|
||||
int rotation = getRotation(file);
|
||||
boolean rotated = rotation == 90 || rotation == 270;
|
||||
int imageHeight = rotated ? options.outWidth : options.outHeight;
|
||||
int imageWidth = rotated ? options.outHeight : options.outWidth;
|
||||
return new Dimensions(imageHeight, imageWidth);
|
||||
}
|
||||
|
||||
private Dimensions getVideoDimensions(File file) throws NotAVideoFile {
|
||||
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
|
||||
try {
|
||||
metadataRetriever.setDataSource(file.getAbsolutePath());
|
||||
} catch (Exception e) {
|
||||
throw new NotAVideoFile();
|
||||
}
|
||||
String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
|
||||
if (hasVideo == null) {
|
||||
throw new NotAVideoFile();
|
||||
}
|
||||
int rotation = extractRotationFromMediaRetriever(metadataRetriever);
|
||||
boolean rotated = rotation == 90 || rotation == 270;
|
||||
int height;
|
||||
try {
|
||||
String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
|
||||
height = Integer.parseInt(h);
|
||||
} catch (Exception e) {
|
||||
height = -1;
|
||||
}
|
||||
int width;
|
||||
try {
|
||||
String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
|
||||
width = Integer.parseInt(w);
|
||||
} catch (Exception e) {
|
||||
width = -1;
|
||||
}
|
||||
metadataRetriever.release();
|
||||
Log.d(Config.LOGTAG,"extracted video dims "+width+"x"+height);
|
||||
return rotated ? new Dimensions(width, height) : new Dimensions(height, width);
|
||||
}
|
||||
|
||||
private int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) {
|
||||
int rotation;
|
||||
if (Build.VERSION.SDK_INT >= 17) {
|
||||
String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
|
||||
try {
|
||||
rotation = Integer.parseInt(r);
|
||||
} catch (Exception e) {
|
||||
rotation = 0;
|
||||
}
|
||||
} else {
|
||||
rotation = 0;
|
||||
}
|
||||
return rotation;
|
||||
}
|
||||
|
||||
private class Dimensions {
|
||||
public final int width;
|
||||
public final int height;
|
||||
|
||||
public Dimensions(int height, int width) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
private class NotAVideoFile extends Exception {
|
||||
|
||||
}
|
||||
|
||||
|
@ -591,4 +901,45 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static boolean weOwnFile(Context context, Uri uri) {
|
||||
if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
|
||||
return false;
|
||||
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return fileIsInFilesDir(context, uri);
|
||||
} else {
|
||||
return weOwnFileLollipop(uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This is more than hacky but probably way better than doing nothing
|
||||
* Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir
|
||||
* and check against those as well
|
||||
*/
|
||||
private static boolean fileIsInFilesDir(Context context, Uri uri) {
|
||||
try {
|
||||
final String haystack = context.getFilesDir().getParentFile().getCanonicalPath();
|
||||
final String needle = new File(uri.getPath()).getCanonicalPath();
|
||||
return needle.startsWith(haystack);
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static boolean weOwnFileLollipop(Uri uri) {
|
||||
try {
|
||||
File file = new File(uri.getPath());
|
||||
FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor();
|
||||
StructStat st = Os.fstat(fd);
|
||||
return st.st_uid == android.os.Process.myUid();
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.content.Context;
|
|||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.PowerManager;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
|
@ -22,6 +23,7 @@ import java.io.OutputStream;
|
|||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
|
@ -36,6 +38,9 @@ import eu.siacs.conversations.entities.DownloadableFile;
|
|||
public class AbstractConnectionManager {
|
||||
protected XmppConnectionService mXmppConnectionService;
|
||||
|
||||
private static final int UI_REFRESH_THRESHOLD = 250;
|
||||
private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0);
|
||||
|
||||
public AbstractConnectionManager(XmppConnectionService service) {
|
||||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
@ -55,7 +60,7 @@ public class AbstractConnectionManager {
|
|||
}
|
||||
|
||||
public boolean hasStoragePermission() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (!Config.ONLY_INTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return mXmppConnectionService.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
|
||||
} else {
|
||||
return true;
|
||||
|
@ -136,6 +141,15 @@ public class AbstractConnectionManager {
|
|||
}
|
||||
}
|
||||
|
||||
public void updateConversationUi(boolean force) {
|
||||
synchronized (LAST_UI_UPDATE_CALL) {
|
||||
if (force || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get() >= UI_REFRESH_THRESHOLD) {
|
||||
LAST_UI_UPDATE_CALL.set(SystemClock.elapsedRealtime());
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PowerManager.WakeLock createWakeLock(String name) {
|
||||
PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
|
||||
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name);
|
||||
|
|
|
@ -6,11 +6,13 @@ import android.graphics.Paint;
|
|||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Bookmark;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
|
@ -19,8 +21,10 @@ import eu.siacs.conversations.entities.ListItem;
|
|||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.MucOptions;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
|
||||
public class AvatarService {
|
||||
public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
|
||||
|
||||
private static final int FG_COLOR = 0xFFFAFAFA;
|
||||
private static final int TRANSPARENT = 0x00000000;
|
||||
|
@ -40,6 +44,9 @@ public class AvatarService {
|
|||
}
|
||||
|
||||
private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
|
||||
if (contact.isSelf()) {
|
||||
return get(contact.getAccount(),size,cachedOnly);
|
||||
}
|
||||
final String KEY = key(contact, size);
|
||||
Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
|
||||
if (avatar != null || cachedOnly) {
|
||||
|
@ -60,7 +67,7 @@ public class AvatarService {
|
|||
|
||||
public Bitmap get(final MucOptions.User user, final int size, boolean cachedOnly) {
|
||||
Contact c = user.getContact();
|
||||
if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
|
||||
if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null || user.getAvatar() == null)) {
|
||||
return get(c, size, cachedOnly);
|
||||
} else {
|
||||
return getImpl(user, size, cachedOnly);
|
||||
|
@ -95,6 +102,9 @@ public class AvatarService {
|
|||
key(contact, size));
|
||||
}
|
||||
}
|
||||
for(Conversation conversation : mXmppConnectionService.findAllConferencesWith(contact)) {
|
||||
clear(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
private String key(Contact contact, int size) {
|
||||
|
@ -162,7 +172,7 @@ public class AvatarService {
|
|||
if (bitmap != null || cachedOnly) {
|
||||
return bitmap;
|
||||
}
|
||||
final List<MucOptions.User> users = mucOptions.getUsers();
|
||||
final List<MucOptions.User> users = mucOptions.getUsers(5);
|
||||
int count = users.size();
|
||||
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
|
@ -227,8 +237,7 @@ public class AvatarService {
|
|||
if (avatar != null || cachedOnly) {
|
||||
return avatar;
|
||||
}
|
||||
avatar = mXmppConnectionService.getFileBackend().getAvatar(
|
||||
account.getAvatar(), size);
|
||||
avatar = mXmppConnectionService.getFileBackend().getAvatar(account.getAvatar(), size);
|
||||
if (avatar == null) {
|
||||
avatar = get(account.getJid().toBareJid().toString(), size,false);
|
||||
}
|
||||
|
@ -243,7 +252,7 @@ public class AvatarService {
|
|||
if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
|
||||
return get(c, size, cachedOnly);
|
||||
} else if (message.getConversation().getMode() == Conversation.MODE_MULTI){
|
||||
MucOptions.User user = conversation.getMucOptions().findUser(message.getCounterpart().getResourcepart());
|
||||
MucOptions.User user = conversation.getMucOptions().findUserByFullJid(message.getCounterpart());
|
||||
if (user != null) {
|
||||
return getImpl(user,size,cachedOnly);
|
||||
}
|
||||
|
@ -292,7 +301,7 @@ public class AvatarService {
|
|||
}
|
||||
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
final String trimmedName = name.trim();
|
||||
final String trimmedName = name == null ? "" : name.trim();
|
||||
drawTile(canvas, trimmedName, 0, 0, size, size);
|
||||
mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
|
||||
return bitmap;
|
||||
|
@ -367,7 +376,7 @@ public class AvatarService {
|
|||
|
||||
private boolean drawTile(Canvas canvas, String name, int left, int top, int right, int bottom) {
|
||||
if (name != null) {
|
||||
final String letter = name.isEmpty() ? "X" : name.substring(0, 1);
|
||||
final String letter = getFirstLetter(name);
|
||||
final int color = UIHelper.getColorForName(name);
|
||||
drawTile(canvas, letter, color, left, top, right, bottom);
|
||||
return true;
|
||||
|
@ -375,6 +384,15 @@ public class AvatarService {
|
|||
return false;
|
||||
}
|
||||
|
||||
private static String getFirstLetter(String name) {
|
||||
for(Character c : name.toCharArray()) {
|
||||
if (Character.isLetterOrDigit(c)) {
|
||||
return c.toString();
|
||||
}
|
||||
}
|
||||
return "X";
|
||||
}
|
||||
|
||||
private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) {
|
||||
if (uri != null) {
|
||||
Bitmap bitmap = mXmppConnectionService.getFileBackend()
|
||||
|
@ -387,10 +405,20 @@ public class AvatarService {
|
|||
return false;
|
||||
}
|
||||
|
||||
private boolean drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop,
|
||||
int dstright, int dstbottom) {
|
||||
private boolean drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop, int dstright, int dstbottom) {
|
||||
Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
|
||||
canvas.drawBitmap(bm, null, dst, null);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdvancedStreamFeaturesAvailable(Account account) {
|
||||
XmppConnection.Features features = account.getXmppConnection().getFeatures();
|
||||
if (features.pep() && !features.pepPersistent()) {
|
||||
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": has pep but is not persistent");
|
||||
if (account.getAvatar() != null) {
|
||||
mXmppConnectionService.republishAvatarIfNeeded(account);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,206 @@
|
|||
package eu.siacs.conversations.services;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.net.Uri;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.IBinder;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.EncodeHintType;
|
||||
import com.google.zxing.aztec.AztecWriter;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Hashtable;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
||||
public class BarcodeProvider extends ContentProvider implements ServiceConnection {
|
||||
|
||||
private static final String AUTHORITY = ".barcodes";
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
private XmppConnectionService mXmppConnectionService;
|
||||
private boolean mBindingInProcess = false;
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
File barcodeDirectory = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/");
|
||||
if (barcodeDirectory.exists() && barcodeDirectory.isDirectory()) {
|
||||
for (File file : barcodeDirectory.listFiles()) {
|
||||
if (file.isFile() && !file.isHidden()) {
|
||||
Log.d(Config.LOGTAG, "deleting old barcode file " + file.getAbsolutePath());
|
||||
file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
||||
return openFile(uri, mode, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openFile(Uri uri, String mode, CancellationSignal signal) throws FileNotFoundException {
|
||||
Log.d(Config.LOGTAG, "opening file with uri (normal): " + uri.toString());
|
||||
String path = uri.getPath();
|
||||
if (path != null && path.endsWith(".png") && path.length() >= 5) {
|
||||
String jid = path.substring(1).substring(0, path.length() - 4);
|
||||
Log.d(Config.LOGTAG, "account:" + jid);
|
||||
if (connectAndWait()) {
|
||||
Log.d(Config.LOGTAG, "connected to background service");
|
||||
try {
|
||||
Account account = mXmppConnectionService.findAccountByJid(Jid.fromString(jid));
|
||||
if (account != null) {
|
||||
String shareableUri = account.getShareableUri();
|
||||
String hash = CryptoHelper.getFingerprint(shareableUri);
|
||||
File file = new File(getContext().getCacheDir().getAbsolutePath() + "/barcodes/" + hash);
|
||||
if (!file.exists()) {
|
||||
file.getParentFile().mkdirs();
|
||||
file.createNewFile();
|
||||
Bitmap bitmap = createAztecBitmap(account.getShareableUri(), 1024);
|
||||
OutputStream outputStream = new FileOutputStream(file);
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
|
||||
outputStream.close();
|
||||
outputStream.flush();
|
||||
}
|
||||
return ParcelFileDescriptor.open(file,ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
|
||||
private boolean connectAndWait() {
|
||||
Intent intent = new Intent(getContext(), XmppConnectionService.class);
|
||||
intent.setAction(this.getClass().getSimpleName());
|
||||
Context context = getContext();
|
||||
if (context != null) {
|
||||
synchronized (this) {
|
||||
if (mXmppConnectionService == null && !mBindingInProcess) {
|
||||
Log.d(Config.LOGTAG,"calling to bind service");
|
||||
context.startService(intent);
|
||||
context.bindService(intent, this, Context.BIND_AUTO_CREATE);
|
||||
this.mBindingInProcess = true;
|
||||
}
|
||||
}
|
||||
try {
|
||||
waitForService();
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "context was null");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
synchronized (this) {
|
||||
XmppConnectionService.XmppConnectionBinder binder = (XmppConnectionService.XmppConnectionBinder) service;
|
||||
mXmppConnectionService = binder.getService();
|
||||
mBindingInProcess = false;
|
||||
synchronized (this.lock) {
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
synchronized (this) {
|
||||
mXmppConnectionService = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void waitForService() throws InterruptedException {
|
||||
if (mXmppConnectionService == null) {
|
||||
synchronized (this.lock) {
|
||||
lock.wait();
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,"not waiting for service because already initialized");
|
||||
}
|
||||
}
|
||||
|
||||
public static Uri getUriForAccount(Context context, Account account) {
|
||||
final String packageId = context.getPackageName();
|
||||
return Uri.parse("content://" + packageId + AUTHORITY + "/" + account.getJid().toBareJid() + ".png");
|
||||
}
|
||||
|
||||
public static Bitmap createAztecBitmap(String input, int size) {
|
||||
try {
|
||||
final AztecWriter AZTEC_WRITER = new AztecWriter();
|
||||
final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
|
||||
hints.put(EncodeHintType.ERROR_CORRECTION, 10);
|
||||
final BitMatrix result = AZTEC_WRITER.encode(input, BarcodeFormat.AZTEC, size, size, hints);
|
||||
final int width = result.getWidth();
|
||||
final int height = result.getHeight();
|
||||
final int[] pixels = new int[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
final int offset = y * width;
|
||||
for (int x = 0; x < width; x++) {
|
||||
pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
}
|
||||
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
|
||||
return bitmap;
|
||||
} catch (final Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ public class ContactChooserTargetService extends ChooserTargetService implements
|
|||
final Conversation conversation = conversations.get(i);
|
||||
final String name = conversation.getName();
|
||||
final Icon icon = Icon.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation, pixel));
|
||||
final float score = (1.0f / MAX_TARGETS) * i;
|
||||
final float score = 1 - (1.0f / MAX_TARGETS) * i;
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putString("uuid", conversation.getUuid());
|
||||
chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras));
|
||||
|
|
|
@ -3,7 +3,9 @@ package eu.siacs.conversations.services;
|
|||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivityManager;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.persistance.DatabaseBackend;
|
||||
|
||||
public class EventReceiver extends BroadcastReceiver {
|
||||
|
@ -16,8 +18,8 @@ public class EventReceiver extends BroadcastReceiver {
|
|||
} else {
|
||||
mIntentForService.setAction("other");
|
||||
}
|
||||
if (intent.getAction().equals("ui")
|
||||
|| DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
|
||||
final String action = intent.getAction();
|
||||
if (action.equals("ui") || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
|
||||
context.startService(mIntentForService);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import eu.siacs.conversations.xmpp.jid.Jid;
|
|||
public class ExportLogsService extends Service {
|
||||
|
||||
private static final SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
||||
private static final String DIRECTORY_STRING_FORMAT = FileBackend.getConversationsFileDirectory() + "/logs/%s";
|
||||
private static final String DIRECTORY_STRING_FORMAT = FileBackend.getConversationsLogsDirectory() + "/logs/%s";
|
||||
private static final String MESSAGE_STRING_FORMAT = "(%s) %s: %s\n";
|
||||
private static final int NOTIFICATION_ID = 1;
|
||||
private static AtomicBoolean running = new AtomicBoolean(false);
|
||||
|
@ -45,9 +45,9 @@ public class ExportLogsService extends Service {
|
|||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
running.set(false);
|
||||
export();
|
||||
stopForeground(true);
|
||||
running.set(false);
|
||||
stopSelf();
|
||||
}
|
||||
}).start();
|
||||
|
|
|
@ -24,13 +24,13 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
|
||||
private final HashSet<Query> queries = new HashSet<Query>();
|
||||
private final ArrayList<Query> pendingQueries = new ArrayList<Query>();
|
||||
private final HashSet<Query> queries = new HashSet<>();
|
||||
private final ArrayList<Query> pendingQueries = new ArrayList<>();
|
||||
|
||||
public enum PagingOrder {
|
||||
NORMAL,
|
||||
REVERSE
|
||||
};
|
||||
}
|
||||
|
||||
public MessageArchiveService(final XmppConnectionService service) {
|
||||
this.mXmppConnectionService = service;
|
||||
|
@ -45,8 +45,20 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
}
|
||||
long startCatchup = getLastMessageTransmitted(account);
|
||||
final Pair<Long,String> lastMessageReceived = mXmppConnectionService.databaseBackend.getLastMessageReceived(account);
|
||||
final Pair<Long,String> lastClearDate = mXmppConnectionService.databaseBackend.getLastClearDate(account);
|
||||
long startCatchup;
|
||||
final String reference;
|
||||
if (lastMessageReceived != null && lastMessageReceived.first >= lastClearDate.first) {
|
||||
startCatchup = lastMessageReceived.first;
|
||||
reference = lastMessageReceived.second;
|
||||
} else {
|
||||
startCatchup = lastClearDate.first;
|
||||
reference = null;
|
||||
}
|
||||
startCatchup = Math.max(startCatchup,mXmppConnectionService.getAutomaticMessageDeletionDate());
|
||||
long endCatchup = account.getXmppConnection().getLastSessionEstablished();
|
||||
final Query query;
|
||||
if (startCatchup == 0) {
|
||||
return;
|
||||
} else if (endCatchup - startCatchup >= Config.MAM_MAX_CATCHUP) {
|
||||
|
@ -57,8 +69,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
this.query(conversation,startCatchup);
|
||||
}
|
||||
}
|
||||
query = new Query(account, startCatchup, endCatchup);
|
||||
} else {
|
||||
query = new Query(account, startCatchup, endCatchup);
|
||||
query.reference = reference;
|
||||
}
|
||||
final Query query = new Query(account, startCatchup, endCatchup);
|
||||
this.queries.add(query);
|
||||
this.execute(query);
|
||||
}
|
||||
|
@ -75,11 +90,6 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
private long getLastMessageTransmitted(final Account account) {
|
||||
Pair<Long,String> pair = mXmppConnectionService.databaseBackend.getLastMessageReceived(account);
|
||||
return pair == null ? 0 : pair.first;
|
||||
}
|
||||
|
||||
public Query query(final Conversation conversation) {
|
||||
if (conversation.getLastMessageTransmitted() < 0 && conversation.countMessages() == 0) {
|
||||
return query(conversation,
|
||||
|
@ -98,10 +108,15 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
|
||||
public Query query(Conversation conversation, long start, long end) {
|
||||
synchronized (this.queries) {
|
||||
final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
|
||||
if (start==0) {
|
||||
query.reference = conversation.getFirstMamReference();
|
||||
Log.d(Config.LOGTAG,"setting mam reference");
|
||||
}
|
||||
query.start = Math.max(start,mXmppConnectionService.getAutomaticMessageDeletionDate());
|
||||
if (start > end) {
|
||||
return null;
|
||||
}
|
||||
final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
|
||||
this.queries.add(query);
|
||||
this.execute(query);
|
||||
return query;
|
||||
|
@ -136,12 +151,12 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
synchronized (MessageArchiveService.this.queries) {
|
||||
MessageArchiveService.this.queries.remove(query);
|
||||
if (query.hasCallback()) {
|
||||
query.callback();
|
||||
query.callback(false);
|
||||
}
|
||||
}
|
||||
} else if (packet.getType() != IqPacket.TYPE.RESULT) {
|
||||
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString());
|
||||
finalizeQuery(query);
|
||||
finalizeQuery(query, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -152,14 +167,14 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
private void finalizeQuery(Query query) {
|
||||
private void finalizeQuery(Query query, boolean done) {
|
||||
synchronized (this.queries) {
|
||||
this.queries.remove(query);
|
||||
}
|
||||
final Conversation conversation = query.getConversation();
|
||||
if (conversation != null) {
|
||||
conversation.sort();
|
||||
conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0);
|
||||
conversation.setHasMessagesLeftOnServer(!done);
|
||||
} else {
|
||||
for(Conversation tmp : this.mXmppConnectionService.getConversations()) {
|
||||
if (tmp.getAccount() == query.getAccount()) {
|
||||
|
@ -168,7 +183,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
if (query.hasCallback()) {
|
||||
query.callback();
|
||||
query.callback(done);
|
||||
} else {
|
||||
this.mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
|
@ -188,6 +203,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean queryInProgress(Conversation conversation) {
|
||||
return queryInProgress(conversation, null);
|
||||
}
|
||||
|
||||
public void processFin(Element fin, Jid from) {
|
||||
if (fin == null) {
|
||||
return;
|
||||
|
@ -202,11 +221,15 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
Element first = set == null ? null : set.findChild("first");
|
||||
Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first;
|
||||
boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES;
|
||||
if (query.getConversation() != null) {
|
||||
query.getConversation().setFirstMamReference(first == null ? null : first.getContent());
|
||||
}
|
||||
if (complete || relevant == null || abort) {
|
||||
this.finalizeQuery(query);
|
||||
Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages");
|
||||
final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() <= mXmppConnectionService.getAutomaticMessageDeletionDate();
|
||||
this.finalizeQuery(query, done);
|
||||
Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done));
|
||||
if (query.getWith() == null && query.getMessageCount() > 0) {
|
||||
mXmppConnectionService.getNotificationService().finishBacklog(true);
|
||||
mXmppConnectionService.getNotificationService().finishBacklog(true,query.getAccount());
|
||||
}
|
||||
} else {
|
||||
final Query nextQuery;
|
||||
|
@ -216,9 +239,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
nextQuery = query.prev(first == null ? null : first.getContent());
|
||||
}
|
||||
this.execute(nextQuery);
|
||||
this.finalizeQuery(query);
|
||||
this.finalizeQuery(query, false);
|
||||
synchronized (this.queries) {
|
||||
this.queries.remove(query);
|
||||
this.queries.add(nextQuery);
|
||||
}
|
||||
}
|
||||
|
@ -274,7 +296,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
this.end = end;
|
||||
this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
|
||||
}
|
||||
|
||||
|
||||
private Query page(String reference) {
|
||||
Query query = new Query(this.account,this.start,this.end);
|
||||
query.reference = reference;
|
||||
|
@ -324,10 +346,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void callback() {
|
||||
public void callback(boolean done) {
|
||||
if (this.callback != null) {
|
||||
this.callback.onMoreMessagesLoaded(messageCount,conversation);
|
||||
if (messageCount == 0) {
|
||||
if (done) {
|
||||
this.callback.informUser(R.string.no_more_history_on_server);
|
||||
}
|
||||
}
|
||||
|
@ -345,12 +367,9 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return this.account;
|
||||
}
|
||||
|
||||
public void incrementTotalCount() {
|
||||
this.totalCount++;
|
||||
}
|
||||
|
||||
public void incrementMessageCount() {
|
||||
this.messageCount++;
|
||||
this.totalCount++;
|
||||
}
|
||||
|
||||
public int getTotalCount() {
|
||||
|
@ -373,7 +392,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
|
|||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (this.muc()) {
|
||||
builder.append("to="+this.getWith().toString());
|
||||
builder.append("to=");
|
||||
builder.append(this.getWith().toString());
|
||||
} else {
|
||||
builder.append("with=");
|
||||
if (this.getWith() == null) {
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
package eu.siacs.conversations.services;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
|
@ -13,35 +11,37 @@ import android.os.SystemClock;
|
|||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.NotificationCompat.BigPictureStyle;
|
||||
import android.support.v4.app.NotificationCompat.Builder;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.support.v4.app.NotificationManagerCompat;
|
||||
import android.support.v4.app.RemoteInput;
|
||||
import android.text.Html;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.ui.ConversationActivity;
|
||||
import eu.siacs.conversations.ui.ManageAccountActivity;
|
||||
import eu.siacs.conversations.ui.SettingsActivity;
|
||||
import eu.siacs.conversations.ui.TimePreference;
|
||||
import eu.siacs.conversations.utils.GeoHelper;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
|
||||
public class NotificationService {
|
||||
|
||||
private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
|
||||
private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
|
||||
|
@ -66,28 +66,6 @@ public class NotificationService {
|
|||
);
|
||||
}
|
||||
|
||||
public void notifyPebble(final Message message) {
|
||||
final Intent i = new Intent("com.getpebble.action.SEND_NOTIFICATION");
|
||||
|
||||
final Conversation conversation = message.getConversation();
|
||||
final JSONObject jsonData = new JSONObject(new HashMap<String, String>(2) {{
|
||||
put("title", conversation.getName());
|
||||
put("body", message.getBody());
|
||||
}});
|
||||
final String notificationData = new JSONArray().put(jsonData).toString();
|
||||
|
||||
i.putExtra("messageType", "PEBBLE_ALERT");
|
||||
i.putExtra("sender", "Conversations"); /* XXX: Shouldn't be hardcoded, e.g., AbstractGenerator.APP_NAME); */
|
||||
i.putExtra("notificationData", notificationData);
|
||||
// notify Pebble App
|
||||
i.setPackage("com.getpebble.android");
|
||||
mXmppConnectionService.sendBroadcast(i);
|
||||
// notify Gadgetbridge
|
||||
i.setPackage("nodomain.freeyourgadget.gadgetbridge");
|
||||
mXmppConnectionService.sendBroadcast(i);
|
||||
}
|
||||
|
||||
|
||||
public boolean notificationsEnabled() {
|
||||
return mXmppConnectionService.getPreferences().getBoolean("show_notification", true);
|
||||
}
|
||||
|
@ -115,13 +93,35 @@ public class NotificationService {
|
|||
}
|
||||
}
|
||||
|
||||
public void finishBacklog(boolean notify) {
|
||||
public void pushFromDirectReply(final Message message) {
|
||||
synchronized (notifications) {
|
||||
pushToStack(message);
|
||||
updateNotification(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void finishBacklog(boolean notify, Account account) {
|
||||
synchronized (notifications) {
|
||||
mXmppConnectionService.updateUnreadCountBadge();
|
||||
updateNotification(notify);
|
||||
if (account == null || !notify) {
|
||||
updateNotification(notify);
|
||||
} else {
|
||||
boolean hasPendingMessages = false;
|
||||
for(ArrayList<Message> messages : notifications.values()) {
|
||||
if (messages.size() > 0 && messages.get(0).getConversation().getAccount() == account) {
|
||||
hasPendingMessages = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
updateNotification(hasPendingMessages);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void finishBacklog(boolean notify) {
|
||||
finishBacklog(notify,null);
|
||||
}
|
||||
|
||||
private void pushToStack(final Message message) {
|
||||
final String conversationUuid = message.getConversationUuid();
|
||||
if (notifications.containsKey(conversationUuid)) {
|
||||
|
@ -136,10 +136,12 @@ public class NotificationService {
|
|||
public void push(final Message message) {
|
||||
mXmppConnectionService.updateUnreadCountBadge();
|
||||
if (!notify(message)) {
|
||||
Log.d(Config.LOGTAG,message.getConversation().getAccount().getJid().toBareJid()+": suppressing notification because turned off");
|
||||
return;
|
||||
}
|
||||
final boolean isScreenOn = mXmppConnectionService.isInteractive();
|
||||
if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
|
||||
Log.d(Config.LOGTAG,message.getConversation().getAccount().getJid().toBareJid()+": suppressing notification because conversation is open");
|
||||
return;
|
||||
}
|
||||
synchronized (notifications) {
|
||||
|
@ -149,14 +151,14 @@ public class NotificationService {
|
|||
&& !account.inGracePeriod()
|
||||
&& !this.inMiniGracePeriod(account);
|
||||
updateNotification(doNotify);
|
||||
if (doNotify) {
|
||||
notifyPebble(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
synchronized (notifications) {
|
||||
for(ArrayList<Message> messages : notifications.values()) {
|
||||
markAsReadIfHasDirectReply(messages);
|
||||
}
|
||||
notifications.clear();
|
||||
updateNotification(false);
|
||||
}
|
||||
|
@ -164,23 +166,35 @@ public class NotificationService {
|
|||
|
||||
public void clear(final Conversation conversation) {
|
||||
synchronized (notifications) {
|
||||
markAsReadIfHasDirectReply(conversation);
|
||||
notifications.remove(conversation.getUuid());
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
|
||||
notificationManager.cancel(conversation.getUuid(), NOTIFICATION_ID);
|
||||
updateNotification(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void markAsReadIfHasDirectReply(final Conversation conversation) {
|
||||
markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
|
||||
}
|
||||
|
||||
private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
|
||||
if (messages != null && messages.size() > 0) {
|
||||
Message last = messages.get(messages.size() - 1);
|
||||
if (last.getStatus() != Message.STATUS_RECEIVED) {
|
||||
mXmppConnectionService.markRead(last.getConversation(), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotificationColor(final Builder mBuilder) {
|
||||
mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary));
|
||||
mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary500));
|
||||
}
|
||||
|
||||
public void updateNotification(final boolean notify) {
|
||||
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
|
||||
final SharedPreferences preferences = mXmppConnectionService.getPreferences();
|
||||
|
||||
final String ringtone = preferences.getString("notification_ringtone", null);
|
||||
final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true);
|
||||
|
||||
if (notifications.size() == 0) {
|
||||
notificationManager.cancel(NOTIFICATION_ID);
|
||||
} else {
|
||||
|
@ -188,31 +202,46 @@ public class NotificationService {
|
|||
this.markLastNotification();
|
||||
}
|
||||
final Builder mBuilder;
|
||||
if (notifications.size() == 1) {
|
||||
mBuilder = buildSingleConversations(notify);
|
||||
if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
mBuilder = buildSingleConversations(notifications.values().iterator().next());
|
||||
modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
|
||||
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
|
||||
} else {
|
||||
mBuilder = buildMultipleConversation();
|
||||
}
|
||||
if (notify && !isQuietHours()) {
|
||||
if (vibrate) {
|
||||
final int dat = 70;
|
||||
final long[] pattern = {0, 3 * dat, dat, dat};
|
||||
mBuilder.setVibrate(pattern);
|
||||
}
|
||||
if (ringtone != null) {
|
||||
mBuilder.setSound(Uri.parse(ringtone));
|
||||
modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
|
||||
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
|
||||
for(Map.Entry<String,ArrayList<Message>> entry : notifications.entrySet()) {
|
||||
Builder singleBuilder = buildSingleConversations(entry.getValue());
|
||||
singleBuilder.setGroup(CONVERSATIONS_GROUP);
|
||||
modifyForSoundVibrationAndLight(singleBuilder,notify,preferences);
|
||||
notificationManager.notify(entry.getKey(), NOTIFICATION_ID ,singleBuilder.build());
|
||||
}
|
||||
}
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, SharedPreferences preferences) {
|
||||
final String ringtone = preferences.getString("notification_ringtone", null);
|
||||
final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true);
|
||||
final boolean led = preferences.getBoolean("led", true);
|
||||
if (notify && !isQuietHours()) {
|
||||
if (vibrate) {
|
||||
final int dat = 70;
|
||||
final long[] pattern = {0, 3 * dat, dat, dat};
|
||||
mBuilder.setVibrate(pattern);
|
||||
}
|
||||
setNotificationColor(mBuilder);
|
||||
mBuilder.setDefaults(0);
|
||||
mBuilder.setSmallIcon(R.drawable.ic_notification);
|
||||
mBuilder.setDeleteIntent(createDeleteIntent());
|
||||
if (ringtone != null) {
|
||||
mBuilder.setSound(Uri.parse(ringtone));
|
||||
}
|
||||
}
|
||||
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
|
||||
}
|
||||
setNotificationColor(mBuilder);
|
||||
mBuilder.setDefaults(0);
|
||||
if (led) {
|
||||
mBuilder.setLights(0xff00FF00, 2000, 3000);
|
||||
final Notification notification = mBuilder.build();
|
||||
notificationManager.notify(NOTIFICATION_ID, notification);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,13 +282,15 @@ public class NotificationService {
|
|||
if (conversation != null) {
|
||||
mBuilder.setContentIntent(createContentIntent(conversation));
|
||||
}
|
||||
mBuilder.setGroupSummary(true);
|
||||
mBuilder.setGroup(CONVERSATIONS_GROUP);
|
||||
mBuilder.setDeleteIntent(createDeleteIntent(null));
|
||||
mBuilder.setSmallIcon(R.drawable.ic_notification);
|
||||
return mBuilder;
|
||||
}
|
||||
|
||||
private Builder buildSingleConversations(final boolean notify) {
|
||||
final Builder mBuilder = new NotificationCompat.Builder(
|
||||
mXmppConnectionService);
|
||||
final ArrayList<Message> messages = notifications.values().iterator().next();
|
||||
private Builder buildSingleConversations(final ArrayList<Message> messages) {
|
||||
final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
|
||||
if (messages.size() >= 1) {
|
||||
final Conversation conversation = messages.get(0).getConversation();
|
||||
mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
|
||||
|
@ -271,11 +302,16 @@ public class NotificationService {
|
|||
} else {
|
||||
Message message;
|
||||
if ((message = getImage(messages)) != null) {
|
||||
modifyForImage(mBuilder, message, messages, notify);
|
||||
} else if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||
modifyForConference(mBuilder, conversation, messages, notify);
|
||||
modifyForImage(mBuilder, message, messages);
|
||||
} else {
|
||||
modifyForTextOnly(mBuilder, messages, notify);
|
||||
modifyForTextOnly(mBuilder, messages);
|
||||
}
|
||||
RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build();
|
||||
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(R.drawable.ic_send_text_offline, "Reply", createReplyIntent(conversation, false)).addRemoteInput(remoteInput).build();
|
||||
NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_send_text_offline, "Reply", createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
|
||||
mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
mBuilder.addAction(replyAction);
|
||||
}
|
||||
if ((message = getFirstDownloadableMessage(messages)) != null) {
|
||||
mBuilder.addAction(
|
||||
|
@ -292,13 +328,23 @@ public class NotificationService {
|
|||
createShowLocationIntent(message));
|
||||
}
|
||||
}
|
||||
if (conversation.getMode() == Conversation.MODE_SINGLE) {
|
||||
Contact contact = conversation.getContact();
|
||||
Uri systemAccount = contact.getSystemAccount();
|
||||
if (systemAccount != null) {
|
||||
mBuilder.addPerson(systemAccount.toString());
|
||||
}
|
||||
}
|
||||
mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
|
||||
mBuilder.setSmallIcon(R.drawable.ic_notification);
|
||||
mBuilder.setDeleteIntent(createDeleteIntent(conversation));
|
||||
mBuilder.setContentIntent(createContentIntent(conversation));
|
||||
}
|
||||
return mBuilder;
|
||||
}
|
||||
|
||||
private void modifyForImage(final Builder builder, final Message message,
|
||||
final ArrayList<Message> messages, final boolean notify) {
|
||||
final ArrayList<Message> messages) {
|
||||
try {
|
||||
final Bitmap bitmap = mXmppConnectionService.getFileBackend()
|
||||
.getThumbnail(message, getPixel(288), false);
|
||||
|
@ -312,8 +358,9 @@ public class NotificationService {
|
|||
final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
|
||||
bigPictureStyle.bigPicture(bitmap);
|
||||
if (tmp.size() > 0) {
|
||||
bigPictureStyle.setSummaryText(getMergedBodies(tmp));
|
||||
builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, tmp.get(0)).first);
|
||||
CharSequence text = getMergedBodies(tmp);
|
||||
bigPictureStyle.setSummaryText(text);
|
||||
builder.setContentText(text);
|
||||
} else {
|
||||
builder.setContentText(mXmppConnectionService.getString(
|
||||
R.string.received_x_file,
|
||||
|
@ -321,55 +368,50 @@ public class NotificationService {
|
|||
}
|
||||
builder.setStyle(bigPictureStyle);
|
||||
} catch (final FileNotFoundException e) {
|
||||
modifyForTextOnly(builder, messages, notify);
|
||||
modifyForTextOnly(builder, messages);
|
||||
}
|
||||
}
|
||||
|
||||
private void modifyForTextOnly(final Builder builder,
|
||||
final ArrayList<Message> messages, final boolean notify) {
|
||||
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
|
||||
builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
|
||||
if (notify) {
|
||||
builder.setTicker(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first);
|
||||
}
|
||||
}
|
||||
|
||||
private void modifyForConference(Builder builder, Conversation conversation, List<Message> messages, boolean notify) {
|
||||
final Message first = messages.get(0);
|
||||
final Message last = messages.get(messages.size() - 1);
|
||||
final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
|
||||
style.setBigContentTitle(conversation.getName());
|
||||
|
||||
for(Message message : messages) {
|
||||
if (message.hasMeCommand()) {
|
||||
style.addLine(UIHelper.getMessagePreview(mXmppConnectionService,message).first);
|
||||
} else {
|
||||
style.addLine(Html.fromHtml("<b>" + UIHelper.getMessageDisplayName(message) + "</b>: " + UIHelper.getMessagePreview(mXmppConnectionService, message).first));
|
||||
private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(mXmppConnectionService.getString(R.string.me));
|
||||
Conversation conversation = messages.get(0).getConversation();
|
||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||
messagingStyle.setConversationTitle(conversation.getName());
|
||||
}
|
||||
}
|
||||
builder.setContentText((first.hasMeCommand() ? "" :UIHelper.getMessageDisplayName(first)+ ": ") +UIHelper.getMessagePreview(mXmppConnectionService, first).first);
|
||||
builder.setStyle(style);
|
||||
if (notify) {
|
||||
builder.setTicker((last.hasMeCommand() ? "" : UIHelper.getMessageDisplayName(last) + ": ") + UIHelper.getMessagePreview(mXmppConnectionService,last).first);
|
||||
for (Message message : messages) {
|
||||
String sender = message.getStatus() == Message.STATUS_RECEIVED ? UIHelper.getMessageDisplayName(message) : null;
|
||||
messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService,message).first, message.getTimeSent(), sender);
|
||||
}
|
||||
builder.setStyle(messagingStyle);
|
||||
} else {
|
||||
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
|
||||
builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get((messages.size()-1))).first);
|
||||
}
|
||||
}
|
||||
|
||||
private Message getImage(final Iterable<Message> messages) {
|
||||
Message image = null;
|
||||
for (final Message message : messages) {
|
||||
if (message.getStatus() != Message.STATUS_RECEIVED) {
|
||||
return null;
|
||||
}
|
||||
if (message.getType() != Message.TYPE_TEXT
|
||||
&& message.getTransferable() == null
|
||||
&& message.getEncryption() != Message.ENCRYPTION_PGP
|
||||
&& message.getFileParams().height > 0) {
|
||||
return message;
|
||||
image = message;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return image;
|
||||
}
|
||||
|
||||
private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
|
||||
for (final Message message : messages) {
|
||||
if ((message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) &&
|
||||
message.getTransferable() != null) {
|
||||
if (message.getTransferable() != null
|
||||
&& (message.getType() == Message.TYPE_FILE
|
||||
|| message.getType() == Message.TYPE_IMAGE
|
||||
|| message.treatAsDownloadable() != Message.Decision.NEVER)) {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
@ -407,28 +449,21 @@ public class NotificationService {
|
|||
}
|
||||
|
||||
private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
|
||||
final TaskStackBuilder stackBuilder = TaskStackBuilder
|
||||
.create(mXmppConnectionService);
|
||||
stackBuilder.addParentStack(ConversationActivity.class);
|
||||
|
||||
final Intent viewConversationIntent = new Intent(mXmppConnectionService,
|
||||
ConversationActivity.class);
|
||||
final Intent viewConversationIntent = new Intent(mXmppConnectionService,ConversationActivity.class);
|
||||
viewConversationIntent.setAction(ConversationActivity.ACTION_VIEW_CONVERSATION);
|
||||
viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid);
|
||||
if (downloadMessageUuid != null) {
|
||||
viewConversationIntent.setAction(ConversationActivity.ACTION_DOWNLOAD);
|
||||
viewConversationIntent.putExtra(ConversationActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
|
||||
return PendingIntent.getActivity(mXmppConnectionService,
|
||||
conversationUuid.hashCode() % 389782,
|
||||
viewConversationIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
} else {
|
||||
viewConversationIntent.setAction(Intent.ACTION_VIEW);
|
||||
return PendingIntent.getActivity(mXmppConnectionService,
|
||||
conversationUuid.hashCode() % 936236,
|
||||
viewConversationIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
if (conversationUuid != null) {
|
||||
viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid);
|
||||
viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
|
||||
}
|
||||
if (downloadMessageUuid != null) {
|
||||
viewConversationIntent.putExtra(ConversationActivity.MESSAGE, downloadMessageUuid);
|
||||
}
|
||||
|
||||
stackBuilder.addNextIntent(viewConversationIntent);
|
||||
|
||||
return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
private PendingIntent createDownloadIntent(final Message message) {
|
||||
|
@ -439,13 +474,25 @@ public class NotificationService {
|
|||
return createContentIntent(conversation.getUuid(), null);
|
||||
}
|
||||
|
||||
private PendingIntent createDeleteIntent() {
|
||||
final Intent intent = new Intent(mXmppConnectionService,
|
||||
XmppConnectionService.class);
|
||||
private PendingIntent createDeleteIntent(Conversation conversation) {
|
||||
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
|
||||
intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
|
||||
if (conversation != null) {
|
||||
intent.putExtra("uuid", conversation.getUuid());
|
||||
return PendingIntent.getService(mXmppConnectionService, conversation.getUuid().hashCode() % 247527, intent, 0);
|
||||
}
|
||||
return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
|
||||
}
|
||||
|
||||
private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
|
||||
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
|
||||
intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
|
||||
intent.putExtra("uuid",conversation.getUuid());
|
||||
intent.putExtra("dismiss_notification",dismissAfterReply);
|
||||
int id = conversation.getUuid().hashCode() % (dismissAfterReply ? 402359 : 426583);
|
||||
return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
|
||||
}
|
||||
|
||||
private PendingIntent createDisableForeground() {
|
||||
final Intent intent = new Intent(mXmppConnectionService,
|
||||
XmppConnectionService.class);
|
||||
|
@ -459,11 +506,10 @@ public class NotificationService {
|
|||
return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
|
||||
}
|
||||
|
||||
private PendingIntent createDisableAccountIntent(final Account account) {
|
||||
private PendingIntent createDismissErrorIntent() {
|
||||
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
|
||||
intent.setAction(XmppConnectionService.ACTION_DISABLE_ACCOUNT);
|
||||
intent.putExtra("account", account.getJid().toBareJid().toString());
|
||||
return PendingIntent.getService(mXmppConnectionService, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
|
||||
return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
|
||||
}
|
||||
|
||||
private boolean wasHighlightedOrPrivate(final Message message) {
|
||||
|
@ -476,7 +522,7 @@ public class NotificationService {
|
|||
return (m.find() || message.getType() == Message.TYPE_PRIVATE);
|
||||
}
|
||||
|
||||
private static Pattern generateNickHighlightPattern(final String nick) {
|
||||
public static Pattern generateNickHighlightPattern(final String nick) {
|
||||
// We expect a word boundary, i.e. space or start of string, followed by
|
||||
// the
|
||||
// nick (matched in case-insensitive manner), followed by optional
|
||||
|
@ -533,17 +579,19 @@ public class NotificationService {
|
|||
mBuilder.setContentIntent(createOpenConversationsIntent());
|
||||
mBuilder.setWhen(0);
|
||||
mBuilder.setPriority(Config.SHOW_CONNECTED_ACCOUNTS ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_MIN);
|
||||
final int cancelIcon;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
mBuilder.setCategory(Notification.CATEGORY_SERVICE);
|
||||
cancelIcon = R.drawable.ic_cancel_white_24dp;
|
||||
} else {
|
||||
cancelIcon = R.drawable.ic_action_cancel;
|
||||
}
|
||||
mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp);
|
||||
mBuilder.addAction(cancelIcon,
|
||||
mXmppConnectionService.getString(R.string.disable_foreground_service),
|
||||
createDisableForeground());
|
||||
if (Config.SHOW_DISABLE_FOREGROUND) {
|
||||
final int cancelIcon;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
mBuilder.setCategory(Notification.CATEGORY_SERVICE);
|
||||
cancelIcon = R.drawable.ic_cancel_white_24dp;
|
||||
} else {
|
||||
cancelIcon = R.drawable.ic_action_cancel;
|
||||
}
|
||||
mBuilder.addAction(cancelIcon,
|
||||
mXmppConnectionService.getString(R.string.disable_foreground_service),
|
||||
createDisableForeground());
|
||||
}
|
||||
return mBuilder.build();
|
||||
}
|
||||
|
||||
|
@ -552,14 +600,14 @@ public class NotificationService {
|
|||
}
|
||||
|
||||
public void updateErrorNotification() {
|
||||
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
|
||||
final List<Account> errors = new ArrayList<>();
|
||||
for (final Account account : mXmppConnectionService.getAccounts()) {
|
||||
if (account.hasErrorStatus()) {
|
||||
if (account.hasErrorStatus() && account.showErrorNotification()) {
|
||||
errors.add(account);
|
||||
}
|
||||
}
|
||||
if (mXmppConnectionService.getPreferences().getBoolean("keep_foreground_service", false)) {
|
||||
if (mXmppConnectionService.getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false)) {
|
||||
notificationManager.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
|
||||
}
|
||||
final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
|
||||
|
@ -576,27 +624,17 @@ public class NotificationService {
|
|||
mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
|
||||
mXmppConnectionService.getString(R.string.try_again),
|
||||
createTryAgainIntent());
|
||||
if (errors.size() == 1) {
|
||||
mBuilder.addAction(R.drawable.ic_block_white_24dp,
|
||||
mXmppConnectionService.getString(R.string.disable_account),
|
||||
createDisableAccountIntent(errors.get(0)));
|
||||
}
|
||||
mBuilder.setOngoing(true);
|
||||
//mBuilder.setLights(0xffffffff, 2000, 4000);
|
||||
mBuilder.setDeleteIntent(createDismissErrorIntent());
|
||||
mBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
|
||||
} else {
|
||||
mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
|
||||
}
|
||||
final TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService);
|
||||
stackBuilder.addParentStack(ConversationActivity.class);
|
||||
|
||||
final Intent manageAccountsIntent = new Intent(mXmppConnectionService, ManageAccountActivity.class);
|
||||
stackBuilder.addNextIntent(manageAccountsIntent);
|
||||
|
||||
final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
mBuilder.setContentIntent(resultPendingIntent);
|
||||
mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService,
|
||||
145,
|
||||
new Intent(mXmppConnectionService,ManageAccountActivity.class),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT));
|
||||
notificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.res.Resources;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
|
||||
|
@ -10,6 +12,12 @@ public class AboutActivity extends Activity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Boolean dark = PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
|
||||
.getString("theme", "light").equals("dark");
|
||||
int mTheme = dark ? R.style.ConversationsTheme_Dark : R.style.ConversationsTheme;
|
||||
setTheme(mTheme);
|
||||
|
||||
setContentView(R.layout.activity_about);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,14 @@ package eu.siacs.conversations.ui;
|
|||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Blockable;
|
||||
|
@ -15,16 +23,30 @@ public final class BlockContactDialog {
|
|||
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
final boolean isBlocked = blockable.isBlocked();
|
||||
builder.setNegativeButton(R.string.cancel, null);
|
||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
LinearLayout view = (LinearLayout) inflater.inflate(R.layout.dialog_block_contact,null);
|
||||
TextView message = (TextView) view.findViewById(R.id.text);
|
||||
final CheckBox report = (CheckBox) view.findViewById(R.id.report_spam);
|
||||
final boolean reporting = blockable.getAccount().getXmppConnection().getFeatures().spamReporting();
|
||||
report.setVisibility(!isBlocked && reporting ? View.VISIBLE : View.GONE);
|
||||
builder.setView(view);
|
||||
|
||||
String value;
|
||||
SpannableString spannable;
|
||||
if (blockable.getJid().isDomainJid() || blockable.getAccount().isBlocked(blockable.getJid().toDomainJid())) {
|
||||
builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain);
|
||||
builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text,
|
||||
blockable.getJid().toDomainJid()));
|
||||
value = blockable.getJid().toDomainJid().toString();
|
||||
spannable = new SpannableString(context.getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text, value));
|
||||
} else {
|
||||
builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact);
|
||||
builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text,
|
||||
blockable.getJid().toBareJid()));
|
||||
value = blockable.getJid().toBareJid().toString();
|
||||
spannable = new SpannableString(context.getString(isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text, value));
|
||||
}
|
||||
int start = spannable.toString().indexOf(value);
|
||||
if (start >= 0) {
|
||||
spannable.setSpan(new TypefaceSpan("monospace"),start,start + value.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
message.setText(spannable);
|
||||
builder.setPositiveButton(isBlocked ? R.string.unblock : R.string.block, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
|
@ -32,7 +54,7 @@ public final class BlockContactDialog {
|
|||
if (isBlocked) {
|
||||
xmppConnectionService.sendUnblockRequest(blockable);
|
||||
} else {
|
||||
xmppConnectionService.sendBlockRequest(blockable);
|
||||
xmppConnectionService.sendBlockRequest(blockable, report.isChecked());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -49,7 +49,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
|
|||
if (account != null) {
|
||||
for (final Jid jid : account.getBlocklist()) {
|
||||
final Contact contact = account.getRoster().getContact(jid);
|
||||
if (contact.match(needle) && contact.isBlocked()) {
|
||||
if (contact.match(this, needle) && contact.isBlocked()) {
|
||||
getListItems().add(contact);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import eu.siacs.conversations.R;
|
||||
|
@ -22,7 +24,7 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
|
|||
final String currentPassword = mCurrentPassword.getText().toString();
|
||||
final String newPassword = mNewPassword.getText().toString();
|
||||
final String newPasswordConfirm = mNewPasswordConfirm.getText().toString();
|
||||
if (!currentPassword.equals(mAccount.getPassword())) {
|
||||
if (!mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && !currentPassword.equals(mAccount.getPassword())) {
|
||||
mCurrentPassword.requestFocus();
|
||||
mCurrentPassword.setError(getString(R.string.account_status_unauthorized));
|
||||
} else if (!newPassword.equals(newPasswordConfirm)) {
|
||||
|
@ -43,6 +45,7 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
|
|||
}
|
||||
}
|
||||
};
|
||||
private TextView mCurrentPasswordLabel;
|
||||
private EditText mCurrentPassword;
|
||||
private EditText mNewPassword;
|
||||
private EditText mNewPasswordConfirm;
|
||||
|
@ -51,7 +54,13 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
|
|||
@Override
|
||||
void onBackendConnected() {
|
||||
this.mAccount = extractAccount(getIntent());
|
||||
|
||||
if (this.mAccount != null && this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
|
||||
this.mCurrentPasswordLabel.setVisibility(View.GONE);
|
||||
this.mCurrentPassword.setVisibility(View.GONE);
|
||||
} else {
|
||||
this.mCurrentPasswordLabel.setVisibility(View.VISIBLE);
|
||||
this.mCurrentPassword.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -67,11 +76,23 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
|
|||
});
|
||||
this.mChangePasswordButton = (Button) findViewById(R.id.right_button);
|
||||
this.mChangePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
|
||||
this.mCurrentPasswordLabel = (TextView) findViewById(R.id.current_password_label);
|
||||
this.mCurrentPassword = (EditText) findViewById(R.id.current_password);
|
||||
this.mNewPassword = (EditText) findViewById(R.id.new_password);
|
||||
this.mNewPasswordConfirm = (EditText) findViewById(R.id.new_password_confirm);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
Intent intent = getIntent();
|
||||
String password = intent != null ? intent.getStringExtra("password") : null;
|
||||
if (password != null) {
|
||||
this.mNewPassword.getEditableText().clear();
|
||||
this.mNewPassword.getEditableText().append(password);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPasswordChangeSucceeded() {
|
||||
runOnUiThread(new Runnable() {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -32,6 +34,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
|
|||
|
||||
private Set<Contact> selected;
|
||||
private Set<String> filterContacts;
|
||||
public static final String EXTRA_TITLE_RES_ID = "extra_title_res_id";
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
|
@ -77,6 +80,8 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
|
|||
String[] selection = getSelectedContactJids();
|
||||
data.putExtra("contacts", selection);
|
||||
data.putExtra("multiple", true);
|
||||
data.putExtra(EXTRA_ACCOUNT,request.getStringExtra(EXTRA_ACCOUNT));
|
||||
data.putExtra("subject", request.getStringExtra("subject"));
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
return true;
|
||||
|
@ -121,6 +126,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
|
|||
data.putExtra("conversation",
|
||||
request.getStringExtra("conversation"));
|
||||
data.putExtra("multiple", false);
|
||||
data.putExtra("subject", request.getStringExtra("subject"));
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
}
|
||||
|
@ -128,6 +134,22 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
|
|||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
Intent intent = getIntent();
|
||||
@StringRes
|
||||
int res = intent != null ? intent.getIntExtra(EXTRA_TITLE_RES_ID,R.string.title_activity_choose_contact) : R.string.title_activity_choose_contact;
|
||||
ActionBar bar = getActionBar();
|
||||
if (bar != null) {
|
||||
try {
|
||||
bar.setTitle(res);
|
||||
} catch (Exception e) {
|
||||
bar.setTitle(R.string.title_activity_choose_contact);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(final Menu menu) {
|
||||
super.onCreateOptionsMenu(menu);
|
||||
|
@ -144,7 +166,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
|
|||
for (final Contact contact : account.getRoster().getContacts()) {
|
||||
if (contact.showInRoster() &&
|
||||
!filterContacts.contains(contact.getJid().toBareJid().toString())
|
||||
&& contact.match(needle)) {
|
||||
&& contact.match(this, needle)) {
|
||||
getListItems().add(contact);
|
||||
}
|
||||
}
|
||||
|
@ -194,6 +216,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
|
|||
data.putExtra("conversation",
|
||||
request.getStringExtra("conversation"));
|
||||
data.putExtra("multiple", false);
|
||||
data.putExtra("subject", request.getStringExtra("subject"));
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.IntentSender.SendIntentException;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.ContextMenu;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -27,7 +24,6 @@ import org.openintents.openpgp.util.OpenPgpUtils;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
|
@ -46,6 +42,9 @@ import eu.siacs.conversations.xmpp.jid.Jid;
|
|||
|
||||
public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed {
|
||||
public static final String ACTION_VIEW_MUC = "view_muc";
|
||||
|
||||
private static final float INACTIVE_ALPHA = 0.4684f; //compromise between dark and light theme
|
||||
|
||||
private Conversation mConversation;
|
||||
private OnClickListener inviteListener = new OnClickListener() {
|
||||
|
||||
|
@ -253,6 +252,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
@Override
|
||||
public void onClick(View v) {
|
||||
quickEdit(mConversation.getMucOptions().getActualNick(),
|
||||
0,
|
||||
new OnValueEdited() {
|
||||
|
||||
@Override
|
||||
|
@ -271,6 +271,15 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
this.mNotifyStatusText = (TextView) findViewById(R.id.notification_status_text);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
final int theme = findTheme();
|
||||
if (this.mTheme != theme) {
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
switch (menuItem.getItemId()) {
|
||||
|
@ -279,9 +288,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
break;
|
||||
case R.id.action_edit_subject:
|
||||
if (mConversation != null) {
|
||||
quickEdit(mConversation.getName(),this.onSubjectEdited);
|
||||
quickEdit(mConversation.getMucOptions().getSubject(),
|
||||
R.string.edit_subject_hint,
|
||||
this.onSubjectEdited);
|
||||
}
|
||||
break;
|
||||
case R.id.action_share:
|
||||
shareUri();
|
||||
break;
|
||||
case R.id.action_save_as_bookmark:
|
||||
saveAsBookmark();
|
||||
break;
|
||||
|
@ -334,7 +348,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.muc_details, menu);
|
||||
return true;
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -349,13 +363,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
final Contact contact = user.getContact();
|
||||
if (contact != null) {
|
||||
name = contact.getDisplayName();
|
||||
} else if (user.getJid() != null){
|
||||
name = user.getJid().toBareJid().toString();
|
||||
} else if (user.getRealJid() != null){
|
||||
name = user.getRealJid().toBareJid().toString();
|
||||
} else {
|
||||
name = user.getName();
|
||||
}
|
||||
menu.setHeaderTitle(name);
|
||||
if (user.getJid() != null) {
|
||||
if (user.getRealJid() != null) {
|
||||
MenuItem showContactDetails = menu.findItem(R.id.action_contact_details);
|
||||
MenuItem startConversation = menu.findItem(R.id.start_conversation);
|
||||
MenuItem giveMembership = menu.findItem(R.id.give_membership);
|
||||
|
@ -364,9 +378,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
MenuItem removeAdminPrivileges = menu.findItem(R.id.remove_admin_privileges);
|
||||
MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room);
|
||||
MenuItem banFromConference = menu.findItem(R.id.ban_from_conference);
|
||||
MenuItem invite = menu.findItem(R.id.invite);
|
||||
startConversation.setVisible(true);
|
||||
if (contact != null) {
|
||||
showContactDetails.setVisible(true);
|
||||
showContactDetails.setVisible(!contact.isSelf());
|
||||
}
|
||||
if (user.getRole() == MucOptions.Role.NONE) {
|
||||
invite.setVisible(true);
|
||||
}
|
||||
if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) &&
|
||||
self.getAffiliation().outranks(user.getAffiliation())) {
|
||||
|
@ -388,7 +406,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
}
|
||||
} else {
|
||||
MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message);
|
||||
sendPrivateMessage.setVisible(true);
|
||||
sendPrivateMessage.setVisible(user.getRole().ranks(MucOptions.Role.PARTICIPANT));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -397,6 +415,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
Jid jid = mSelectedUser.getRealJid();
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_contact_details:
|
||||
Contact contact = mSelectedUser.getContact();
|
||||
|
@ -408,27 +427,32 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
startConversation(mSelectedUser);
|
||||
return true;
|
||||
case R.id.give_admin_privileges:
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.ADMIN,this);
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.ADMIN,this);
|
||||
return true;
|
||||
case R.id.give_membership:
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this);
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.MEMBER,this);
|
||||
return true;
|
||||
case R.id.remove_membership:
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.NONE,this);
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.NONE,this);
|
||||
return true;
|
||||
case R.id.remove_admin_privileges:
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this);
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.MEMBER,this);
|
||||
return true;
|
||||
case R.id.remove_from_room:
|
||||
removeFromRoom(mSelectedUser);
|
||||
return true;
|
||||
case R.id.ban_from_conference:
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this);
|
||||
xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,this);
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,jid, MucOptions.Affiliation.OUTCAST,this);
|
||||
if (mSelectedUser.getRole() != MucOptions.Role.NONE) {
|
||||
xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, this);
|
||||
}
|
||||
return true;
|
||||
case R.id.send_private_message:
|
||||
privateMsgInMuc(mConversation,mSelectedUser.getName());
|
||||
return true;
|
||||
case R.id.invite:
|
||||
xmppConnectionService.directInvite(mConversation, jid);
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
@ -436,8 +460,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
|
||||
private void removeFromRoom(final User user) {
|
||||
if (mConversation.getMucOptions().membersOnly()) {
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.NONE,this);
|
||||
xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this);
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,user.getRealJid(), MucOptions.Affiliation.NONE,this);
|
||||
if (user.getRole() != MucOptions.Role.NONE) {
|
||||
xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, ConferenceDetailsActivity.this);
|
||||
}
|
||||
} else {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.ban_from_conference);
|
||||
|
@ -446,8 +472,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
builder.setPositiveButton(R.string.ban_now,new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.OUTCAST,ConferenceDetailsActivity.this);
|
||||
xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this);
|
||||
xmppConnectionService.changeAffiliationInConference(mConversation,user.getRealJid(), MucOptions.Affiliation.OUTCAST,ConferenceDetailsActivity.this);
|
||||
if (user.getRole() != MucOptions.Role.NONE) {
|
||||
xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, ConferenceDetailsActivity.this);
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.create().show();
|
||||
|
@ -455,23 +483,15 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
}
|
||||
|
||||
protected void startConversation(User user) {
|
||||
if (user.getJid() != null) {
|
||||
Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(),user.getJid().toBareJid(),false);
|
||||
if (user.getRealJid() != null) {
|
||||
Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(),user.getRealJid().toBareJid(),false);
|
||||
switchToConversation(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
protected void saveAsBookmark() {
|
||||
Account account = mConversation.getAccount();
|
||||
Bookmark bookmark = new Bookmark(account, mConversation.getJid().toBareJid());
|
||||
if (!mConversation.getJid().isBareJid()) {
|
||||
bookmark.setNick(mConversation.getJid().getResourcepart());
|
||||
}
|
||||
bookmark.setBookmarkName(mConversation.getMucOptions().getSubject());
|
||||
bookmark.setAutojoin(getPreferences().getBoolean("autojoin",true));
|
||||
account.getBookmarks().add(bookmark);
|
||||
xmppConnectionService.pushBookmarks(account);
|
||||
mConversation.setBookmark(bookmark);
|
||||
xmppConnectionService.saveConversationAsBookmark(mConversation,
|
||||
mConversation.getMucOptions().getSubject());
|
||||
}
|
||||
|
||||
protected void deleteBookmark() {
|
||||
|
@ -492,8 +512,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
this.uuid = getIntent().getExtras().getString("uuid");
|
||||
}
|
||||
if (uuid != null) {
|
||||
this.mConversation = xmppConnectionService
|
||||
.findConversationByUuid(uuid);
|
||||
this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
|
||||
if (this.mConversation != null) {
|
||||
updateView();
|
||||
}
|
||||
|
@ -501,6 +520,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
}
|
||||
|
||||
private void updateView() {
|
||||
invalidateOptionsMenu();
|
||||
final MucOptions mucOptions = mConversation.getMucOptions();
|
||||
final User self = mucOptions.getSelf();
|
||||
String account;
|
||||
|
@ -541,30 +561,30 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
}
|
||||
}
|
||||
|
||||
int ic_notifications = getThemeResource(R.attr.icon_notifications, R.drawable.ic_notifications_black54_24dp);
|
||||
int ic_notifications_off = getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black54_24dp);
|
||||
int ic_notifications_paused = getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black54_24dp);
|
||||
int ic_notifications_none = getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black54_24dp);
|
||||
|
||||
long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0);
|
||||
if (mutedTill == Long.MAX_VALUE) {
|
||||
mNotifyStatusText.setText(R.string.notify_never);
|
||||
mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_off_grey600_24dp);
|
||||
mNotifyStatusButton.setImageResource(ic_notifications_off);
|
||||
} else if (System.currentTimeMillis() < mutedTill) {
|
||||
mNotifyStatusText.setText(R.string.notify_paused);
|
||||
mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_paused_grey600_24dp);
|
||||
mNotifyStatusButton.setImageResource(ic_notifications_paused);
|
||||
} else if (mConversation.alwaysNotify()) {
|
||||
mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_grey600_24dp);
|
||||
mNotifyStatusButton.setImageResource(ic_notifications);
|
||||
mNotifyStatusText.setText(R.string.notify_on_all_messages);
|
||||
} else {
|
||||
mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_none_grey600_24dp);
|
||||
mNotifyStatusButton.setImageResource(ic_notifications_none);
|
||||
mNotifyStatusText.setText(R.string.notify_only_when_highlighted);
|
||||
}
|
||||
|
||||
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
membersView.removeAllViews();
|
||||
final ArrayList<User> users = mucOptions.getUsers();
|
||||
Collections.sort(users,new Comparator<User>() {
|
||||
@Override
|
||||
public int compare(User lhs, User rhs) {
|
||||
return lhs.getName().compareToIgnoreCase(rhs.getName());
|
||||
}
|
||||
});
|
||||
Collections.sort(users);
|
||||
for (final User user : users) {
|
||||
View view = inflater.inflate(R.layout.contact, membersView,false);
|
||||
this.setListItemBackgroundOnView(view);
|
||||
|
@ -591,16 +611,23 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
tvKey.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
|
||||
}
|
||||
Contact contact = user.getContact();
|
||||
String name = user.getName();
|
||||
if (contact != null) {
|
||||
tvDisplayName.setText(contact.getDisplayName());
|
||||
tvStatus.setText(user.getName() + " \u2022 " + getStatus(user));
|
||||
tvStatus.setText((name != null ? name+ " \u2022 " : "") + getStatus(user));
|
||||
} else {
|
||||
tvDisplayName.setText(user.getName());
|
||||
tvDisplayName.setText(name == null ? "" : name);
|
||||
tvStatus.setText(getStatus(user));
|
||||
|
||||
}
|
||||
ImageView iv = (ImageView) view.findViewById(R.id.contact_photo);
|
||||
iv.setImageBitmap(avatarService().get(user, getPixel(48), false));
|
||||
if (user.getRole() == MucOptions.Role.NONE) {
|
||||
tvDisplayName.setAlpha(INACTIVE_ALPHA);
|
||||
tvKey.setAlpha(INACTIVE_ALPHA);
|
||||
tvStatus.setAlpha(INACTIVE_ALPHA);
|
||||
iv.setAlpha(INACTIVE_ALPHA);
|
||||
}
|
||||
membersView.addView(view);
|
||||
if (mConversation.getMucOptions().canInvite()) {
|
||||
mInviteButton.setVisibility(View.VISIBLE);
|
||||
|
@ -623,26 +650,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
private void setListItemBackgroundOnView(View view) {
|
||||
int sdk = android.os.Build.VERSION.SDK_INT;
|
||||
if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
|
||||
} else {
|
||||
view.setBackground(getResources().getDrawable(R.drawable.greybackground));
|
||||
}
|
||||
}
|
||||
|
||||
private void viewPgpKey(User user) {
|
||||
PgpEngine pgp = xmppConnectionService.getPgpEngine();
|
||||
if (pgp != null) {
|
||||
PendingIntent intent = pgp.getIntentForKey(
|
||||
mConversation.getAccount(), user.getPgpKeyId());
|
||||
PendingIntent intent = pgp.getIntentForKey(user.getPgpKeyId());
|
||||
if (intent != null) {
|
||||
try {
|
||||
startIntentSenderForResult(intent.getIntentSender(), 0,
|
||||
null, 0, 0, 0);
|
||||
startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0);
|
||||
} catch (SendIntentException ignored) {
|
||||
|
||||
}
|
||||
|
@ -652,7 +666,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
|
|||
|
||||
@Override
|
||||
public void onAffiliationChangedSuccessful(Jid jid) {
|
||||
|
||||
refreshUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -10,11 +10,9 @@ import android.content.SharedPreferences;
|
|||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.ContactsContract.CommonDataKinds;
|
||||
import android.provider.ContactsContract.Contacts;
|
||||
import android.provider.ContactsContract.Intents;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
@ -30,15 +28,17 @@ import android.widget.QuickContactBadge;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.wefika.flowlayout.FlowLayout;
|
||||
|
||||
import org.openintents.openpgp.util.OpenPgpUtils;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.PgpEngine;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Contact;
|
||||
|
@ -47,13 +47,14 @@ import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
|
|||
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
|
||||
import eu.siacs.conversations.utils.CryptoHelper;
|
||||
import eu.siacs.conversations.utils.UIHelper;
|
||||
import eu.siacs.conversations.utils.XmppUri;
|
||||
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
|
||||
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
||||
public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
|
||||
public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
|
||||
public static final String ACTION_VIEW_CONTACT = "view_contact";
|
||||
|
||||
private Contact contact;
|
||||
|
@ -104,17 +105,22 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
}
|
||||
};
|
||||
private Jid accountJid;
|
||||
private TextView lastseen;
|
||||
private Jid contactJid;
|
||||
private TextView contactJidTv;
|
||||
private TextView accountJidTv;
|
||||
private TextView lastseen;
|
||||
private TextView statusMessage;
|
||||
private CheckBox send;
|
||||
private CheckBox receive;
|
||||
private Button addContactButton;
|
||||
private Button mShowInactiveDevicesButton;
|
||||
private QuickContactBadge badge;
|
||||
private LinearLayout keys;
|
||||
private LinearLayout tags;
|
||||
private boolean showDynamicTags;
|
||||
private LinearLayout keysWrapper;
|
||||
private FlowLayout tags;
|
||||
private boolean showDynamicTags = false;
|
||||
private boolean showLastSeen = false;
|
||||
private boolean showInactiveOmemo = false;
|
||||
private String messageFingerprint;
|
||||
|
||||
private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
|
||||
|
@ -135,22 +141,20 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (contact.getSystemAccount() == null) {
|
||||
Uri systemAccount = contact.getSystemAccount();
|
||||
if (systemAccount == null) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(
|
||||
ContactDetailsActivity.this);
|
||||
builder.setTitle(getString(R.string.action_add_phone_book));
|
||||
builder.setMessage(getString(R.string.add_phone_book_text,
|
||||
contact.getJid()));
|
||||
contact.getDisplayJid()));
|
||||
builder.setNegativeButton(getString(R.string.cancel), null);
|
||||
builder.setPositiveButton(getString(R.string.add), addToPhonebook);
|
||||
builder.create().show();
|
||||
} else {
|
||||
String[] systemAccount = contact.getSystemAccount().split("#");
|
||||
long id = Long.parseLong(systemAccount[0]);
|
||||
Uri uri = ContactsContract.Contacts.getLookupUri(id, systemAccount[1]);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(uri);
|
||||
startActivity(intent);
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setData(systemAccount);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -179,7 +183,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
@Override
|
||||
protected String getShareableUri() {
|
||||
if (contact != null) {
|
||||
return contact.getShareableUri();
|
||||
return "xmpp:"+contact.getJid().toBareJid().toString();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
@ -188,6 +192,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo",false);
|
||||
if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
|
||||
try {
|
||||
this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT));
|
||||
|
@ -204,6 +209,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
contactJidTv = (TextView) findViewById(R.id.details_contactjid);
|
||||
accountJidTv = (TextView) findViewById(R.id.details_account);
|
||||
lastseen = (TextView) findViewById(R.id.details_lastseen);
|
||||
statusMessage = (TextView) findViewById(R.id.status_message);
|
||||
send = (CheckBox) findViewById(R.id.details_send_presence);
|
||||
receive = (CheckBox) findViewById(R.id.details_receive_presence);
|
||||
badge = (QuickContactBadge) findViewById(R.id.details_contact_badge);
|
||||
|
@ -215,14 +221,39 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
}
|
||||
});
|
||||
keys = (LinearLayout) findViewById(R.id.details_contact_keys);
|
||||
tags = (LinearLayout) findViewById(R.id.tags);
|
||||
keysWrapper = (LinearLayout) findViewById(R.id.keys_wrapper);
|
||||
tags = (FlowLayout) findViewById(R.id.tags);
|
||||
mShowInactiveDevicesButton = (Button) findViewById(R.id.show_inactive_devices);
|
||||
if (getActionBar() != null) {
|
||||
getActionBar().setHomeButtonEnabled(true);
|
||||
getActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
mShowInactiveDevicesButton.setOnClickListener(new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
showInactiveOmemo = !showInactiveOmemo;
|
||||
populateView();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false);
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle savedInstanceState) {
|
||||
savedInstanceState.putBoolean("show_inactive_omemo",showInactiveOmemo);
|
||||
super.onSaveInstanceState(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
final int theme = findTheme();
|
||||
if (this.mTheme != theme) {
|
||||
recreate();
|
||||
} else {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
this.showDynamicTags = preferences.getBoolean("show_dynamic_tags", false);
|
||||
this.showLastSeen = preferences.getBoolean("last_activity", false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -233,17 +264,21 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
case android.R.id.home:
|
||||
finish();
|
||||
break;
|
||||
case R.id.action_share:
|
||||
shareUri();
|
||||
break;
|
||||
case R.id.action_delete_contact:
|
||||
builder.setTitle(getString(R.string.action_delete_contact))
|
||||
.setMessage(
|
||||
getString(R.string.remove_contact_text,
|
||||
contact.getJid()))
|
||||
contact.getDisplayJid()))
|
||||
.setPositiveButton(getString(R.string.delete),
|
||||
removeFromRoster).create().show();
|
||||
break;
|
||||
case R.id.action_edit_contact:
|
||||
if (contact.getSystemAccount() == null) {
|
||||
quickEdit(contact.getDisplayName(), new OnValueEdited() {
|
||||
Uri systemAccount = contact.getSystemAccount();
|
||||
if (systemAccount == null) {
|
||||
quickEdit(contact.getDisplayName(), 0, new OnValueEdited() {
|
||||
|
||||
@Override
|
||||
public void onValueEdited(String value) {
|
||||
|
@ -255,10 +290,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
});
|
||||
} else {
|
||||
Intent intent = new Intent(Intent.ACTION_EDIT);
|
||||
String[] systemAccount = contact.getSystemAccount().split("#");
|
||||
long id = Long.parseLong(systemAccount[0]);
|
||||
Uri uri = Contacts.getLookupUri(id, systemAccount[1]);
|
||||
intent.setDataAndType(uri, Contacts.CONTENT_ITEM_TYPE);
|
||||
intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
|
||||
intent.putExtra("finishActivityOnSaveCompleted", true);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
@ -298,10 +330,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
edit.setVisible(false);
|
||||
delete.setVisible(false);
|
||||
}
|
||||
return true;
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
private void populateView() {
|
||||
if (contact == null) {
|
||||
return;
|
||||
}
|
||||
invalidateOptionsMenu();
|
||||
setTitle(contact.getDisplayName());
|
||||
if (contact.showInRoster()) {
|
||||
|
@ -311,6 +346,25 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
send.setOnCheckedChangeListener(null);
|
||||
receive.setOnCheckedChangeListener(null);
|
||||
|
||||
List<String> statusMessages = contact.getPresences().getStatusMessages();
|
||||
if (statusMessages.size() == 0) {
|
||||
statusMessage.setVisibility(View.GONE);
|
||||
} else {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
statusMessage.setVisibility(View.VISIBLE);
|
||||
int s = statusMessages.size();
|
||||
for(int i = 0; i < s; ++i) {
|
||||
if (s > 1) {
|
||||
builder.append("• ");
|
||||
}
|
||||
builder.append(statusMessages.get(i));
|
||||
if (i < s - 1) {
|
||||
builder.append("\n");
|
||||
}
|
||||
}
|
||||
statusMessage.setText(builder);
|
||||
}
|
||||
|
||||
if (contact.getOption(Contact.Options.FROM)) {
|
||||
send.setText(R.string.send_presence_updates);
|
||||
send.setChecked(true);
|
||||
|
@ -343,26 +397,32 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
receive.setEnabled(false);
|
||||
send.setEnabled(false);
|
||||
}
|
||||
|
||||
send.setOnCheckedChangeListener(this.mOnSendCheckedChange);
|
||||
receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
|
||||
} else {
|
||||
addContactButton.setVisibility(View.VISIBLE);
|
||||
send.setVisibility(View.GONE);
|
||||
receive.setVisibility(View.GONE);
|
||||
statusMessage.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (contact.isBlocked() && !this.showDynamicTags) {
|
||||
lastseen.setVisibility(View.VISIBLE);
|
||||
lastseen.setText(R.string.contact_blocked);
|
||||
} else {
|
||||
lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.lastseen.time));
|
||||
if (showLastSeen && contact.getLastseen() > 0) {
|
||||
lastseen.setVisibility(View.VISIBLE);
|
||||
lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
|
||||
} else {
|
||||
lastseen.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (contact.getPresences().size() > 1) {
|
||||
contactJidTv.setText(contact.getJid() + " ("
|
||||
contactJidTv.setText(contact.getDisplayJid() + " ("
|
||||
+ contact.getPresences().size() + ")");
|
||||
} else {
|
||||
contactJidTv.setText(contact.getJid().toString());
|
||||
contactJidTv.setText(contact.getDisplayJid());
|
||||
}
|
||||
String account;
|
||||
if (Config.DOMAIN_LOCK != null) {
|
||||
|
@ -377,40 +437,69 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
keys.removeAllViews();
|
||||
boolean hasKeys = false;
|
||||
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
for(final String otrFingerprint : contact.getOtrFingerprints()) {
|
||||
hasKeys = true;
|
||||
View view = inflater.inflate(R.layout.contact_key, keys, false);
|
||||
TextView key = (TextView) view.findViewById(R.id.key);
|
||||
TextView keyType = (TextView) view.findViewById(R.id.key_type);
|
||||
ImageButton removeButton = (ImageButton) view
|
||||
.findViewById(R.id.button_remove);
|
||||
removeButton.setVisibility(View.VISIBLE);
|
||||
keyType.setText("OTR Fingerprint");
|
||||
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
|
||||
keys.addView(view);
|
||||
removeButton.setOnClickListener(new OnClickListener() {
|
||||
if (Config.supportOtr()) {
|
||||
for (final String otrFingerprint : contact.getOtrFingerprints()) {
|
||||
hasKeys = true;
|
||||
View view = inflater.inflate(R.layout.contact_key, keys, false);
|
||||
TextView key = (TextView) view.findViewById(R.id.key);
|
||||
TextView keyType = (TextView) view.findViewById(R.id.key_type);
|
||||
ImageButton removeButton = (ImageButton) view
|
||||
.findViewById(R.id.button_remove);
|
||||
removeButton.setVisibility(View.VISIBLE);
|
||||
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
|
||||
if (otrFingerprint != null && otrFingerprint.equals(messageFingerprint)) {
|
||||
keyType.setText(R.string.otr_fingerprint_selected_message);
|
||||
keyType.setTextColor(getResources().getColor(R.color.accent));
|
||||
} else {
|
||||
keyType.setText(R.string.otr_fingerprint);
|
||||
}
|
||||
keys.addView(view);
|
||||
removeButton.setOnClickListener(new OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
confirmToDeleteFingerprint(otrFingerprint);
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
confirmToDeleteFingerprint(otrFingerprint);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
for (final String fingerprint : contact.getAccount().getAxolotlService().getFingerprintsForContact(contact)) {
|
||||
boolean highlight = fingerprint.equals(messageFingerprint);
|
||||
hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight, new OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onOmemoKeyClicked(contact.getAccount(), fingerprint);
|
||||
if (Config.supportOmemo()) {
|
||||
boolean skippedInactive = false;
|
||||
boolean showsInactive = false;
|
||||
for (final XmppAxolotlSession session : contact.getAccount().getAxolotlService().findSessionsForContact(contact)) {
|
||||
final FingerprintStatus trust = session.getTrust();
|
||||
if (!trust.isActive()) {
|
||||
if (showInactiveOmemo) {
|
||||
showsInactive = true;
|
||||
} else {
|
||||
skippedInactive = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!trust.isCompromised()) {
|
||||
boolean highlight = session.getFingerprint().equals(messageFingerprint);
|
||||
hasKeys = true;
|
||||
addFingerprintRow(keys, session, highlight);
|
||||
}
|
||||
}
|
||||
if (showsInactive || skippedInactive) {
|
||||
mShowInactiveDevicesButton.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
|
||||
mShowInactiveDevicesButton.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mShowInactiveDevicesButton.setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
mShowInactiveDevicesButton.setVisibility(View.GONE);
|
||||
}
|
||||
if (contact.getPgpKeyId() != 0) {
|
||||
if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
|
||||
hasKeys = true;
|
||||
View view = inflater.inflate(R.layout.contact_key, keys, false);
|
||||
TextView key = (TextView) view.findViewById(R.id.key);
|
||||
TextView keyType = (TextView) view.findViewById(R.id.key_type);
|
||||
keyType.setText("PGP Key ID");
|
||||
keyType.setText(R.string.openpgp_key_id);
|
||||
if ("pgp".equals(messageFingerprint)) {
|
||||
keyType.setTextColor(getResources().getColor(R.color.accent));
|
||||
}
|
||||
key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
|
||||
view.setOnClickListener(new OnClickListener() {
|
||||
|
||||
|
@ -434,13 +523,9 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
});
|
||||
keys.addView(view);
|
||||
}
|
||||
if (hasKeys) {
|
||||
keys.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
keys.setVisibility(View.GONE);
|
||||
}
|
||||
keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
|
||||
|
||||
List<ListItem.Tag> tagList = contact.getTags();
|
||||
List<ListItem.Tag> tagList = contact.getTags(this);
|
||||
if (tagList.size() == 0 || !this.showDynamicTags) {
|
||||
tags.setVisibility(View.GONE);
|
||||
} else {
|
||||
|
@ -455,40 +540,6 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
}
|
||||
}
|
||||
|
||||
private void onOmemoKeyClicked(Account account, String fingerprint) {
|
||||
final XmppAxolotlSession.Trust trust = account.getAxolotlService().getFingerprintTrust(fingerprint);
|
||||
if (trust != null && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
|
||||
X509Certificate x509Certificate = account.getAxolotlService().getFingerprintCertificate(fingerprint);
|
||||
if (x509Certificate != null) {
|
||||
showCertificateInformationDialog(CryptoHelper.extractCertificateInformation(x509Certificate));
|
||||
} else {
|
||||
Toast.makeText(this,R.string.certificate_not_found, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void showCertificateInformationDialog(Bundle bundle) {
|
||||
View view = getLayoutInflater().inflate(R.layout.certificate_information, null);
|
||||
final String not_available = getString(R.string.certicate_info_not_available);
|
||||
TextView subject_cn = (TextView) view.findViewById(R.id.subject_cn);
|
||||
TextView subject_o = (TextView) view.findViewById(R.id.subject_o);
|
||||
TextView issuer_cn = (TextView) view.findViewById(R.id.issuer_cn);
|
||||
TextView issuer_o = (TextView) view.findViewById(R.id.issuer_o);
|
||||
TextView sha1 = (TextView) view.findViewById(R.id.sha1);
|
||||
|
||||
subject_cn.setText(bundle.getString("subject_cn", not_available));
|
||||
subject_o.setText(bundle.getString("subject_o", not_available));
|
||||
issuer_cn.setText(bundle.getString("issuer_cn", not_available));
|
||||
issuer_o.setText(bundle.getString("issuer_o", not_available));
|
||||
sha1.setText(bundle.getString("sha1", not_available));
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.certificate_information);
|
||||
builder.setView(view);
|
||||
builder.setPositiveButton(R.string.ok, null);
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
protected void confirmToDeleteFingerprint(final String fingerprint) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.delete_fingerprint);
|
||||
|
@ -509,15 +560,17 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
builder.create().show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackendConnected() {
|
||||
if ((accountJid != null) && (contactJid != null)) {
|
||||
Account account = xmppConnectionService
|
||||
.findAccountByJid(accountJid);
|
||||
if (accountJid != null && contactJid != null) {
|
||||
Account account = xmppConnectionService.findAccountByJid(accountJid);
|
||||
if (account == null) {
|
||||
return;
|
||||
}
|
||||
this.contact = account.getRoster().getContact(contactJid);
|
||||
if (mPendingFingerprintVerificationUri != null) {
|
||||
processFingerprintVerification(mPendingFingerprintVerificationUri);
|
||||
mPendingFingerprintVerificationUri = null;
|
||||
}
|
||||
populateView();
|
||||
}
|
||||
}
|
||||
|
@ -526,4 +579,15 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
|
|||
public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
|
||||
refreshUi();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processFingerprintVerification(XmppUri uri) {
|
||||
if (contact != null && contact.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
|
||||
if (xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints())) {
|
||||
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this,R.string.invalid_barcode,Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import android.app.ActionBar;
|
|||
import android.app.AlertDialog;
|
||||
import android.app.FragmentTransaction;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
|
@ -14,6 +15,7 @@ import android.content.pm.PackageManager;
|
|||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.Settings;
|
||||
import android.support.v4.widget.SlidingPaneLayout;
|
||||
|
@ -47,6 +49,7 @@ import de.timroes.android.listview.EnhancedListView;
|
|||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
|
||||
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.entities.Blockable;
|
||||
|
@ -54,6 +57,7 @@ import eu.siacs.conversations.entities.Contact;
|
|||
import eu.siacs.conversations.entities.Conversation;
|
||||
import eu.siacs.conversations.entities.Message;
|
||||
import eu.siacs.conversations.entities.Transferable;
|
||||
import eu.siacs.conversations.persistance.FileBackend;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
|
||||
import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
|
||||
|
@ -61,17 +65,16 @@ import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
|
|||
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
|
||||
import eu.siacs.conversations.utils.ExceptionHelper;
|
||||
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
|
||||
import eu.siacs.conversations.xmpp.XmppConnection;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
||||
public class ConversationActivity extends XmppActivity
|
||||
implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast {
|
||||
|
||||
public static final String ACTION_DOWNLOAD = "eu.siacs.conversations.action.DOWNLOAD";
|
||||
|
||||
public static final String VIEW_CONVERSATION = "viewConversation";
|
||||
public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
|
||||
public static final String CONVERSATION = "conversationUuid";
|
||||
public static final String MESSAGE = "messageUuid";
|
||||
public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
|
||||
public static final String TEXT = "text";
|
||||
public static final String NICK = "nick";
|
||||
public static final String PRIVATE_MESSAGE = "pm";
|
||||
|
@ -91,9 +94,13 @@ public class ConversationActivity extends XmppActivity
|
|||
private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
|
||||
private static final String STATE_PANEL_OPEN = "state_panel_open";
|
||||
private static final String STATE_PENDING_URI = "state_pending_uri";
|
||||
private static final String STATE_FIRST_VISIBLE = "first_visible";
|
||||
private static final String STATE_OFFSET_FROM_TOP = "offset_from_top";
|
||||
|
||||
private String mOpenConverstaion = null;
|
||||
private String mOpenConversation = null;
|
||||
private boolean mPanelOpen = true;
|
||||
private AtomicBoolean mShouldPanelBeOpen = new AtomicBoolean(false);
|
||||
private Pair<Integer,Integer> mScrollPosition = null;
|
||||
final private List<Uri> mPendingImageUris = new ArrayList<>();
|
||||
final private List<Uri> mPendingFileUris = new ArrayList<>();
|
||||
private Uri mPendingGeoUri = null;
|
||||
|
@ -115,6 +122,7 @@ public class ConversationActivity extends XmppActivity
|
|||
private boolean mActivityPaused = false;
|
||||
private AtomicBoolean mRedirected = new AtomicBoolean(false);
|
||||
private Pair<Integer, Intent> mPostponedActivityResult;
|
||||
private boolean mUnprocessedNewIntent = false;
|
||||
|
||||
public Conversation getSelectedConversation() {
|
||||
return this.mSelectedConversation;
|
||||
|
@ -127,6 +135,7 @@ public class ConversationActivity extends XmppActivity
|
|||
public void showConversationsOverview() {
|
||||
if (mContentView instanceof SlidingPaneLayout) {
|
||||
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
|
||||
mShouldPanelBeOpen.set(true);
|
||||
mSlidingPaneLayout.openPane();
|
||||
}
|
||||
}
|
||||
|
@ -144,22 +153,18 @@ public class ConversationActivity extends XmppActivity
|
|||
public void hideConversationsOverview() {
|
||||
if (mContentView instanceof SlidingPaneLayout) {
|
||||
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
|
||||
mShouldPanelBeOpen.set(false);
|
||||
mSlidingPaneLayout.closePane();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConversationsOverviewHideable() {
|
||||
if (mContentView instanceof SlidingPaneLayout) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
return mContentView instanceof SlidingPaneLayout;
|
||||
}
|
||||
|
||||
public boolean isConversationsOverviewVisable() {
|
||||
if (mContentView instanceof SlidingPaneLayout) {
|
||||
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
|
||||
return mSlidingPaneLayout.isOpen();
|
||||
return mShouldPanelBeOpen.get();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
@ -169,10 +174,19 @@ public class ConversationActivity extends XmppActivity
|
|||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
mOpenConverstaion = savedInstanceState.getString(STATE_OPEN_CONVERSATION, null);
|
||||
mOpenConversation = savedInstanceState.getString(STATE_OPEN_CONVERSATION, null);
|
||||
mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true);
|
||||
int pos = savedInstanceState.getInt(STATE_FIRST_VISIBLE, -1);
|
||||
int offset = savedInstanceState.getInt(STATE_OFFSET_FROM_TOP, 1);
|
||||
if (pos >= 0 && offset <= 0) {
|
||||
Log.d(Config.LOGTAG,"retrieved scroll position from instanceState "+pos+":"+offset);
|
||||
mScrollPosition = new Pair<>(pos,offset);
|
||||
} else {
|
||||
mScrollPosition = null;
|
||||
}
|
||||
String pending = savedInstanceState.getString(STATE_PENDING_URI, null);
|
||||
if (pending != null) {
|
||||
Log.d(Config.LOGTAG,"ConversationsActivity.onCreate() - restoring pending image uri");
|
||||
mPendingImageUris.clear();
|
||||
mPendingImageUris.add(Uri.parse(pending));
|
||||
}
|
||||
|
@ -284,26 +298,25 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
if (mContentView instanceof SlidingPaneLayout) {
|
||||
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
|
||||
mSlidingPaneLayout.setParallaxDistance(150);
|
||||
mSlidingPaneLayout
|
||||
.setShadowResource(R.drawable.es_slidingpane_shadow);
|
||||
mSlidingPaneLayout.setShadowResource(R.drawable.es_slidingpane_shadow);
|
||||
mSlidingPaneLayout.setSliderFadeColor(0);
|
||||
mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() {
|
||||
|
||||
@Override
|
||||
public void onPanelOpened(View arg0) {
|
||||
mShouldPanelBeOpen.set(true);
|
||||
updateActionBarTitle();
|
||||
invalidateOptionsMenu();
|
||||
hideKeyboard();
|
||||
if (xmppConnectionServiceBound) {
|
||||
xmppConnectionService.getNotificationService()
|
||||
.setOpenConversation(null);
|
||||
xmppConnectionService.getNotificationService().setOpenConversation(null);
|
||||
}
|
||||
closeContextMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPanelClosed(View arg0) {
|
||||
mShouldPanelBeOpen.set(false);
|
||||
listView.discardUndo();
|
||||
openConversation();
|
||||
}
|
||||
|
@ -365,12 +378,8 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
|
||||
public void sendReadMarkerIfNecessary(final Conversation conversation) {
|
||||
if (!mActivityPaused && conversation != null) {
|
||||
if (!conversation.isRead()) {
|
||||
xmppConnectionService.sendReadMarker(conversation);
|
||||
} else {
|
||||
xmppConnectionService.markRead(conversation);
|
||||
}
|
||||
if (!mActivityPaused && !mUnprocessedNewIntent && conversation != null) {
|
||||
xmppConnectionService.sendReadMarker(conversation);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -412,9 +421,11 @@ public class ConversationActivity extends XmppActivity
|
|||
menuContactDetails.setVisible(false);
|
||||
menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating());
|
||||
menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
|
||||
menuSecure.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); //if pgp is hidden conferences have no choice of encryption
|
||||
menuSecure.setVisible((Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
|
||||
} else {
|
||||
menuContactDetails.setVisible(!this.getSelectedConversation().withSelf());
|
||||
menuMucDetails.setVisible(false);
|
||||
menuSecure.setVisible(Config.multipleEncryptionChoices());
|
||||
}
|
||||
if (this.getSelectedConversation().isMuted()) {
|
||||
menuMute.setVisible(false);
|
||||
|
@ -423,7 +434,46 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
if (Config.supportOmemo()) {
|
||||
new Handler().post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
View view = findViewById(R.id.action_security);
|
||||
if (view != null) {
|
||||
view.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
return quickOmemoDebugger(getSelectedConversation());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
private boolean quickOmemoDebugger(Conversation c) {
|
||||
if (c != null) {
|
||||
boolean single = c.getMode() == Conversation.MODE_SINGLE;
|
||||
AxolotlService axolotlService = c.getAccount().getAxolotlService();
|
||||
Pair<AxolotlService.AxolotlCapability,Jid> capabilityJidPair = axolotlService.isConversationAxolotlCapableDetailed(c);
|
||||
switch (capabilityJidPair.first) {
|
||||
case MISSING_PRESENCE:
|
||||
Toast.makeText(ConversationActivity.this,single ? getString(R.string.missing_presence_subscription) : getString(R.string.missing_presence_subscription_with_x,capabilityJidPair.second.toBareJid().toString()),Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
case MISSING_KEYS:
|
||||
Toast.makeText(ConversationActivity.this,single ? getString(R.string.missing_omemo_keys) : getString(R.string.missing_keys_from_x,capabilityJidPair.second.toBareJid().toString()),Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
case WRONG_CONFIGURATION:
|
||||
Toast.makeText(ConversationActivity.this,R.string.wrong_conference_configuration, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
case NO_MEMBERS:
|
||||
Toast.makeText(ConversationActivity.this,R.string.this_conference_has_no_members, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
|
||||
|
@ -447,6 +497,8 @@ public class ConversationActivity extends XmppActivity
|
|||
break;
|
||||
case ATTACHMENT_CHOICE_TAKE_PHOTO:
|
||||
Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri();
|
||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
|
||||
mPendingImageUris.clear();
|
||||
|
@ -501,7 +553,7 @@ public class ConversationActivity extends XmppActivity
|
|||
|
||||
public void attachFile(final int attachmentChoice) {
|
||||
if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
|
||||
if (!hasStoragePermission(attachmentChoice)) {
|
||||
if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(attachmentChoice)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -541,7 +593,7 @@ public class ConversationActivity extends XmppActivity
|
|||
|
||||
@Override
|
||||
public void error(int error, Contact contact) {
|
||||
displayErrorDialog(error);
|
||||
replaceToast(getString(error));
|
||||
}
|
||||
});
|
||||
} else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) {
|
||||
|
@ -564,10 +616,8 @@ public class ConversationActivity extends XmppActivity
|
|||
@Override
|
||||
public void onClick(DialogInterface dialog,
|
||||
int which) {
|
||||
conversation
|
||||
.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
xmppConnectionService.databaseBackend
|
||||
.updateConversation(conversation);
|
||||
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
xmppConnectionService.updateConversation(conversation);
|
||||
selectPresenceToAttachFile(attachmentChoice, Message.ENCRYPTION_NONE);
|
||||
}
|
||||
});
|
||||
|
@ -600,7 +650,7 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
|
||||
public void startDownloadable(Message message) {
|
||||
if (!hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
|
||||
if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
|
||||
this.mPendingDownloadableMessage = message;
|
||||
return;
|
||||
}
|
||||
|
@ -825,7 +875,7 @@ public class ConversationActivity extends XmppActivity
|
|||
conversation.setNextEncryption(Message.ENCRYPTION_PGP);
|
||||
item.setChecked(true);
|
||||
} else {
|
||||
announcePgp(conversation.getAccount(), conversation);
|
||||
announcePgp(conversation.getAccount(), conversation, onOpenPGPKeyPublished);
|
||||
}
|
||||
} else {
|
||||
showInstallPgpDialog();
|
||||
|
@ -841,7 +891,7 @@ public class ConversationActivity extends XmppActivity
|
|||
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
|
||||
break;
|
||||
}
|
||||
xmppConnectionService.databaseBackend.updateConversation(conversation);
|
||||
xmppConnectionService.updateConversation(conversation);
|
||||
fragment.updateChatMsgHint();
|
||||
invalidateOptionsMenu();
|
||||
refreshUi();
|
||||
|
@ -853,13 +903,14 @@ public class ConversationActivity extends XmppActivity
|
|||
MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none);
|
||||
MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp);
|
||||
MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl);
|
||||
pgp.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION);
|
||||
none.setVisible(!Config.FORCE_E2E_ENCRYPTION);
|
||||
otr.setVisible(!Config.X509_VERIFICATION);
|
||||
pgp.setVisible(Config.supportOpenPgp());
|
||||
none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI);
|
||||
otr.setVisible(Config.supportOtr());
|
||||
axolotl.setVisible(Config.supportOmemo());
|
||||
if (conversation.getMode() == Conversation.MODE_MULTI) {
|
||||
otr.setVisible(false);
|
||||
axolotl.setVisible(false);
|
||||
} else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
|
||||
}
|
||||
if (!conversation.getAccount().getAxolotlService().isConversationAxolotlCapable(conversation)) {
|
||||
axolotl.setEnabled(false);
|
||||
}
|
||||
switch (conversation.getNextEncryption()) {
|
||||
|
@ -899,8 +950,7 @@ public class ConversationActivity extends XmppActivity
|
|||
till = System.currentTimeMillis() + (durations[which] * 1000);
|
||||
}
|
||||
conversation.setMutedTill(till);
|
||||
ConversationActivity.this.xmppConnectionService.databaseBackend
|
||||
.updateConversation(conversation);
|
||||
ConversationActivity.this.xmppConnectionService.updateConversation(conversation);
|
||||
updateConversationList();
|
||||
ConversationActivity.this.mConversationFragment.updateMessages();
|
||||
invalidateOptionsMenu();
|
||||
|
@ -911,7 +961,7 @@ public class ConversationActivity extends XmppActivity
|
|||
|
||||
public void unmuteConversation(final Conversation conversation) {
|
||||
conversation.setMutedTill(0);
|
||||
this.xmppConnectionService.databaseBackend.updateConversation(conversation);
|
||||
this.xmppConnectionService.updateConversation(conversation);
|
||||
updateConversationList();
|
||||
ConversationActivity.this.mConversationFragment.updateMessages();
|
||||
invalidateOptionsMenu();
|
||||
|
@ -922,7 +972,7 @@ public class ConversationActivity extends XmppActivity
|
|||
if (!isConversationsOverviewVisable()) {
|
||||
showConversationsOverview();
|
||||
} else {
|
||||
moveTaskToBack(true);
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -944,14 +994,18 @@ public class ConversationActivity extends XmppActivity
|
|||
upKey = KeyEvent.KEYCODE_DPAD_RIGHT;
|
||||
downKey = KeyEvent.KEYCODE_DPAD_LEFT;
|
||||
break;
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
upKey = KeyEvent.KEYCODE_DPAD_UP;
|
||||
downKey = KeyEvent.KEYCODE_DPAD_DOWN;
|
||||
}
|
||||
final boolean modifier = event.isCtrlPressed() || event.isAltPressed();
|
||||
final boolean modifier = event.isCtrlPressed() || (event.getMetaState() & KeyEvent.META_ALT_LEFT_ON) != 0;
|
||||
if (modifier && key == KeyEvent.KEYCODE_TAB && isConversationsOverviewHideable()) {
|
||||
toggleConversationsOverview();
|
||||
return true;
|
||||
} else if (modifier && key == KeyEvent.KEYCODE_SPACE) {
|
||||
startActivity(new Intent(this, StartConversationActivity.class));
|
||||
return true;
|
||||
} else if (modifier && key == downKey) {
|
||||
if (isConversationsOverviewHideable() && !isConversationsOverviewVisable()) {
|
||||
showConversationsOverview();
|
||||
|
@ -1036,13 +1090,15 @@ public class ConversationActivity extends XmppActivity
|
|||
|
||||
@Override
|
||||
protected void onNewIntent(final Intent intent) {
|
||||
if (xmppConnectionServiceBound) {
|
||||
if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) {
|
||||
if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
|
||||
mOpenConversation = null;
|
||||
mUnprocessedNewIntent = true;
|
||||
if (xmppConnectionServiceBound) {
|
||||
handleViewConversationIntent(intent);
|
||||
setIntent(new Intent());
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
} else {
|
||||
setIntent(intent);
|
||||
}
|
||||
} else {
|
||||
setIntent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1063,9 +1119,6 @@ public class ConversationActivity extends XmppActivity
|
|||
listView.discardUndo();
|
||||
super.onPause();
|
||||
this.mActivityPaused = true;
|
||||
if (this.xmppConnectionServiceBound) {
|
||||
this.xmppConnectionService.getNotificationService().setIsInForeground(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1077,9 +1130,7 @@ public class ConversationActivity extends XmppActivity
|
|||
recreate();
|
||||
}
|
||||
this.mActivityPaused = false;
|
||||
if (this.xmppConnectionServiceBound) {
|
||||
this.xmppConnectionService.getNotificationService().setIsInForeground(true);
|
||||
}
|
||||
|
||||
|
||||
if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) {
|
||||
sendReadMarkerIfNecessary(getSelectedConversation());
|
||||
|
@ -1092,11 +1143,17 @@ public class ConversationActivity extends XmppActivity
|
|||
Conversation conversation = getSelectedConversation();
|
||||
if (conversation != null) {
|
||||
savedInstanceState.putString(STATE_OPEN_CONVERSATION, conversation.getUuid());
|
||||
Pair<Integer,Integer> scrollPosition = mConversationFragment.getScrollPosition();
|
||||
if (scrollPosition != null) {
|
||||
savedInstanceState.putInt(STATE_FIRST_VISIBLE, scrollPosition.first);
|
||||
savedInstanceState.putInt(STATE_OFFSET_FROM_TOP, scrollPosition.second);
|
||||
}
|
||||
} else {
|
||||
savedInstanceState.remove(STATE_OPEN_CONVERSATION);
|
||||
}
|
||||
savedInstanceState.putBoolean(STATE_PANEL_OPEN, isConversationsOverviewVisable());
|
||||
if (this.mPendingImageUris.size() >= 1) {
|
||||
Log.d(Config.LOGTAG,"ConversationsActivity.onSaveInstanceState() - saving pending image uri");
|
||||
savedInstanceState.putString(STATE_PENDING_URI, this.mPendingImageUris.get(0).toString());
|
||||
} else {
|
||||
savedInstanceState.remove(STATE_PENDING_URI);
|
||||
|
@ -1117,30 +1174,41 @@ public class ConversationActivity extends XmppActivity
|
|||
updateConversationList();
|
||||
|
||||
if (mPendingConferenceInvite != null) {
|
||||
mPendingConferenceInvite.execute(this);
|
||||
if (mPendingConferenceInvite.execute(this)) {
|
||||
mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
|
||||
mToast.show();
|
||||
}
|
||||
mPendingConferenceInvite = null;
|
||||
}
|
||||
|
||||
final Intent intent = getIntent();
|
||||
|
||||
if (xmppConnectionService.getAccounts().size() == 0) {
|
||||
if (mRedirected.compareAndSet(false, true)) {
|
||||
if (Config.X509_VERIFICATION) {
|
||||
startActivity(new Intent(this, ManageAccountActivity.class));
|
||||
} else if (Config.MAGIC_CREATE_DOMAIN != null) {
|
||||
startActivity(new Intent(this, WelcomeActivity.class));
|
||||
} else {
|
||||
startActivity(new Intent(this, EditAccountActivity.class));
|
||||
Intent editAccount = new Intent(this, EditAccountActivity.class);
|
||||
editAccount.putExtra("init",true);
|
||||
startActivity(editAccount);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
} else if (conversationList.size() <= 0) {
|
||||
if (mRedirected.compareAndSet(false, true)) {
|
||||
Intent intent = new Intent(this, StartConversationActivity.class);
|
||||
intent.putExtra("init", true);
|
||||
startActivity(intent);
|
||||
Account pendingAccount = xmppConnectionService.getPendingAccount();
|
||||
if (pendingAccount == null) {
|
||||
Intent startConversationActivity = new Intent(this, StartConversationActivity.class);
|
||||
intent.putExtra("init", true);
|
||||
startActivity(startConversationActivity);
|
||||
} else {
|
||||
switchToAccount(pendingAccount, true);
|
||||
}
|
||||
finish();
|
||||
}
|
||||
} else if (getIntent() != null && VIEW_CONVERSATION.equals(getIntent().getType())) {
|
||||
clearPending();
|
||||
handleViewConversationIntent(getIntent());
|
||||
} else if (selectConversationByUuid(mOpenConverstaion)) {
|
||||
} else if (selectConversationByUuid(mOpenConversation)) {
|
||||
if (mPanelOpen) {
|
||||
showConversationsOverview();
|
||||
} else {
|
||||
|
@ -1149,8 +1217,15 @@ public class ConversationActivity extends XmppActivity
|
|||
updateActionBarTitle(true);
|
||||
}
|
||||
}
|
||||
this.mConversationFragment.reInit(getSelectedConversation());
|
||||
mOpenConverstaion = null;
|
||||
if (this.mConversationFragment.reInit(getSelectedConversation())) {
|
||||
Log.d(Config.LOGTAG,"setting scroll position on fragment");
|
||||
this.mConversationFragment.setScrollPosition(mScrollPosition);
|
||||
}
|
||||
mOpenConversation = null;
|
||||
} else if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
|
||||
clearPending();
|
||||
handleViewConversationIntent(intent);
|
||||
intent.setAction(Intent.ACTION_MAIN);
|
||||
} else if (getSelectedConversation() == null) {
|
||||
showConversationsOverview();
|
||||
clearPending();
|
||||
|
@ -1166,13 +1241,22 @@ public class ConversationActivity extends XmppActivity
|
|||
this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
|
||||
}
|
||||
|
||||
final boolean stopping;
|
||||
if (Build.VERSION.SDK_INT >= 17) {
|
||||
stopping = isFinishing() || isDestroyed();
|
||||
} else {
|
||||
stopping = isFinishing();
|
||||
}
|
||||
|
||||
if (!forbidProcessingPendings) {
|
||||
for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
|
||||
Uri foo = i.next();
|
||||
Log.d(Config.LOGTAG,"ConversationsActivity.onBackendConnected() - attaching image to conversations. stopping="+Boolean.toString(stopping));
|
||||
attachImageToConversation(getSelectedConversation(), foo);
|
||||
}
|
||||
|
||||
for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
|
||||
Log.d(Config.LOGTAG,"ConversationsActivity.onBackendConnected() - attaching file to conversations. stopping="+Boolean.toString(stopping));
|
||||
attachFileToConversation(getSelectedConversation(), i.next());
|
||||
}
|
||||
|
||||
|
@ -1186,12 +1270,16 @@ public class ConversationActivity extends XmppActivity
|
|||
if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) {
|
||||
openBatteryOptimizationDialogIfNeeded();
|
||||
}
|
||||
setIntent(new Intent());
|
||||
if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
|
||||
xmppConnectionService.getNotificationService().setOpenConversation(null);
|
||||
} else {
|
||||
xmppConnectionService.getNotificationService().setOpenConversation(getSelectedConversation());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleViewConversationIntent(final Intent intent) {
|
||||
final String uuid = intent.getStringExtra(CONVERSATION);
|
||||
final String downloadUuid = intent.getStringExtra(MESSAGE);
|
||||
final String downloadUuid = intent.getStringExtra(EXTRA_DOWNLOAD_UUID);
|
||||
final String text = intent.getStringExtra(TEXT);
|
||||
final String nick = intent.getStringExtra(NICK);
|
||||
final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE, false);
|
||||
|
@ -1213,6 +1301,7 @@ public class ConversationActivity extends XmppActivity
|
|||
this.mConversationFragment.appendText(text);
|
||||
}
|
||||
hideConversationsOverview();
|
||||
mUnprocessedNewIntent = false;
|
||||
openConversation();
|
||||
if (mContentView instanceof SlidingPaneLayout) {
|
||||
updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet
|
||||
|
@ -1223,6 +1312,8 @@ public class ConversationActivity extends XmppActivity
|
|||
startDownloadable(message);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mUnprocessedNewIntent = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1253,9 +1344,11 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
Uri uri = intent.getData();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && uri == null) {
|
||||
ClipData clipData = intent.getClipData();
|
||||
for (int i = 0; i < clipData.getItemCount(); ++i) {
|
||||
uris.add(clipData.getItemAt(i).getUri());
|
||||
final ClipData clipData = intent.getClipData();
|
||||
if (clipData != null) {
|
||||
for (int i = 0; i < clipData.getItemCount(); ++i) {
|
||||
uris.add(clipData.getItemAt(i).getUri());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
uris.add(uri);
|
||||
|
@ -1276,7 +1369,7 @@ public class ConversationActivity extends XmppActivity
|
|||
// associate selected PGP keyId with the account
|
||||
mSelectedConversation.getAccount().setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
|
||||
// we need to announce the key as described in XEP-027
|
||||
announcePgp(mSelectedConversation.getAccount(), null);
|
||||
announcePgp(mSelectedConversation.getAccount(), null, onOpenPGPKeyPublished);
|
||||
} else {
|
||||
choosePgpSignId(mSelectedConversation.getAccount());
|
||||
}
|
||||
|
@ -1286,7 +1379,7 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
|
||||
if (xmppConnectionServiceBound) {
|
||||
announcePgp(mSelectedConversation.getAccount(), mSelectedConversation);
|
||||
announcePgp(mSelectedConversation.getAccount(), mSelectedConversation, onOpenPGPKeyPublished);
|
||||
this.mPostponedActivityResult = null;
|
||||
} else {
|
||||
this.mPostponedActivityResult = new Pair<>(requestCode, data);
|
||||
|
@ -1296,27 +1389,47 @@ public class ConversationActivity extends XmppActivity
|
|||
mPendingImageUris.addAll(extractUriFromIntent(data));
|
||||
if (xmppConnectionServiceBound) {
|
||||
for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
|
||||
Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching image to conversations. CHOOSE_IMAGE");
|
||||
attachImageToConversation(getSelectedConversation(), i.next());
|
||||
}
|
||||
}
|
||||
} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE) {
|
||||
mPendingFileUris.clear();
|
||||
mPendingFileUris.addAll(extractUriFromIntent(data));
|
||||
if (xmppConnectionServiceBound) {
|
||||
for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
|
||||
attachFileToConversation(getSelectedConversation(), i.next());
|
||||
final List<Uri> uris = extractUriFromIntent(data);
|
||||
final Conversation c = getSelectedConversation();
|
||||
final OnPresenceSelected callback = new OnPresenceSelected() {
|
||||
@Override
|
||||
public void onPresenceSelected() {
|
||||
mPendingFileUris.clear();
|
||||
mPendingFileUris.addAll(uris);
|
||||
if (xmppConnectionServiceBound) {
|
||||
for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
|
||||
Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE");
|
||||
attachFileToConversation(c, i.next());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
if (c == null || c.getMode() == Conversation.MODE_MULTI
|
||||
|| FileBackend.allFilesUnderSize(this, uris, getMaxHttpUploadSize(c))
|
||||
|| c.getNextEncryption() == Message.ENCRYPTION_OTR) {
|
||||
callback.onPresenceSelected();
|
||||
} else {
|
||||
selectPresence(c, callback);
|
||||
}
|
||||
} else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
|
||||
if (mPendingImageUris.size() == 1) {
|
||||
Uri uri = mPendingImageUris.get(0);
|
||||
Uri uri = FileBackend.getIndexableTakePhotoUri(mPendingImageUris.get(0));
|
||||
mPendingImageUris.set(0, uri);
|
||||
if (xmppConnectionServiceBound) {
|
||||
Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching image to conversations. TAKE_PHOTO");
|
||||
attachImageToConversation(getSelectedConversation(), uri);
|
||||
mPendingImageUris.clear();
|
||||
}
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||
intent.setData(uri);
|
||||
sendBroadcast(intent);
|
||||
if (!Config.ONLY_INTERNAL_STORAGE) {
|
||||
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
|
||||
intent.setData(uri);
|
||||
sendBroadcast(intent);
|
||||
}
|
||||
} else {
|
||||
mPendingImageUris.clear();
|
||||
}
|
||||
|
@ -1350,12 +1463,19 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
}
|
||||
|
||||
private long getMaxHttpUploadSize(Conversation conversation) {
|
||||
final XmppConnection connection = conversation.getAccount().getXmppConnection();
|
||||
return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
|
||||
}
|
||||
|
||||
private void setNeverAskForBatteryOptimizationsAgain() {
|
||||
getPreferences().edit().putBoolean("show_battery_optimization", false).commit();
|
||||
}
|
||||
|
||||
private void openBatteryOptimizationDialogIfNeeded() {
|
||||
if (showBatteryOptimizationWarning() && getPreferences().getBoolean("show_battery_optimization", true)) {
|
||||
if (hasAccountWithoutPush()
|
||||
&& isOptimizingBattery()
|
||||
&& getPreferences().getBoolean("show_battery_optimization", true)) {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setTitle(R.string.battery_optimizations_enabled);
|
||||
builder.setMessage(R.string.battery_optimizations_enabled_dialog);
|
||||
|
@ -1365,7 +1485,11 @@ public class ConversationActivity extends XmppActivity
|
|||
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
Uri uri = Uri.parse("package:" + getPackageName());
|
||||
intent.setData(uri);
|
||||
startActivityForResult(intent, REQUEST_BATTERY_OP);
|
||||
try {
|
||||
startActivityForResult(intent, REQUEST_BATTERY_OP);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Toast.makeText(ConversationActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
|
@ -1380,6 +1504,16 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
}
|
||||
|
||||
private boolean hasAccountWithoutPush() {
|
||||
for(Account account : xmppConnectionService.getAccounts()) {
|
||||
if (account.getStatus() != Account.State.DISABLED
|
||||
&& !xmppConnectionService.getPushManagementService().availableAndUseful(account)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void attachLocationToConversation(Conversation conversation, Uri uri) {
|
||||
if (conversation == null) {
|
||||
return;
|
||||
|
@ -1409,26 +1543,53 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
final Toast prepareFileToast = Toast.makeText(getApplicationContext(),getText(R.string.preparing_file), Toast.LENGTH_LONG);
|
||||
prepareFileToast.show();
|
||||
xmppConnectionService.attachFileToConversation(conversation, uri, new UiCallback<Message>() {
|
||||
xmppConnectionService.attachFileToConversation(conversation, uri, new UiInformableCallback<Message>() {
|
||||
@Override
|
||||
public void inform(final String text) {
|
||||
hidePrepareFileToast(prepareFileToast);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
replaceToast(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success(Message message) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
hideToast();
|
||||
}
|
||||
});
|
||||
hidePrepareFileToast(prepareFileToast);
|
||||
xmppConnectionService.sendMessage(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int errorCode, Message message) {
|
||||
public void error(final int errorCode, Message message) {
|
||||
hidePrepareFileToast(prepareFileToast);
|
||||
displayErrorDialog(errorCode);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
replaceToast(getString(errorCode));
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userInputRequried(PendingIntent pi, Message message) {
|
||||
|
||||
hidePrepareFileToast(prepareFileToast);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void attachImageToConversation(Uri uri) {
|
||||
this.attachImageToConversation(getSelectedConversation(), uri);
|
||||
}
|
||||
|
||||
private void attachImageToConversation(Conversation conversation, Uri uri) {
|
||||
if (conversation == null) {
|
||||
return;
|
||||
|
@ -1450,9 +1611,14 @@ public class ConversationActivity extends XmppActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void error(int error, Message message) {
|
||||
public void error(final int error, Message message) {
|
||||
hidePrepareFileToast(prepareFileToast);
|
||||
displayErrorDialog(error);
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
replaceToast(getString(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1495,21 +1661,30 @@ public class ConversationActivity extends XmppActivity
|
|||
new UiCallback<Message>() {
|
||||
|
||||
@Override
|
||||
public void userInputRequried(PendingIntent pi,
|
||||
Message message) {
|
||||
ConversationActivity.this.runIntent(pi,
|
||||
ConversationActivity.REQUEST_SEND_MESSAGE);
|
||||
public void userInputRequried(PendingIntent pi,Message message) {
|
||||
ConversationActivity.this.runIntent(pi,ConversationActivity.REQUEST_SEND_MESSAGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success(Message message) {
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
xmppConnectionService.sendMessage(message);
|
||||
if (mConversationFragment != null) {
|
||||
mConversationFragment.messageSent();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(int error, Message message) {
|
||||
|
||||
public void error(final int error, Message message) {
|
||||
runOnUiThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
Toast.makeText(ConversationActivity.this,
|
||||
R.string.unable_to_connect_to_keychain,
|
||||
Toast.LENGTH_SHORT
|
||||
).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1522,8 +1697,8 @@ public class ConversationActivity extends XmppActivity
|
|||
return getPreferences().getBoolean("indicate_received", false);
|
||||
}
|
||||
|
||||
public boolean useWhiteBackground() {
|
||||
return getPreferences().getBoolean("use_white_background",false);
|
||||
public boolean useGreenBackground() {
|
||||
return getPreferences().getBoolean("use_green_background",true);
|
||||
}
|
||||
|
||||
protected boolean trustKeysIfNeeded(int requestCode) {
|
||||
|
@ -1532,18 +1707,23 @@ public class ConversationActivity extends XmppActivity
|
|||
|
||||
protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
|
||||
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
|
||||
Contact contact = mSelectedConversation.getContact();
|
||||
boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
|
||||
boolean hasUndecidedContact = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED,contact).isEmpty();
|
||||
final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
|
||||
boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
|
||||
boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
|
||||
boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
|
||||
boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
|
||||
boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0;
|
||||
if(hasUndecidedOwn || hasUndecidedContact || hasPendingKeys || hasNoTrustedKeys) {
|
||||
boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
|
||||
if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
|
||||
axolotlService.createSessionsIfNeeded(mSelectedConversation);
|
||||
Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
|
||||
intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString());
|
||||
String[] contacts = new String[targets.size()];
|
||||
for(int i = 0; i < contacts.length; ++i) {
|
||||
contacts[i] = targets.get(i).toString();
|
||||
}
|
||||
intent.putExtra("contacts", contacts);
|
||||
intent.putExtra(EXTRA_ACCOUNT, mSelectedConversation.getAccount().getJid().toBareJid().toString());
|
||||
intent.putExtra("choice", attachmentChoice);
|
||||
intent.putExtra("has_no_trusted", hasNoTrustedKeys);
|
||||
intent.putExtra("conversation",mSelectedConversation.getUuid());
|
||||
startActivityForResult(intent, requestCode);
|
||||
return true;
|
||||
} else {
|
||||
|
@ -1555,9 +1735,14 @@ public class ConversationActivity extends XmppActivity
|
|||
protected void refreshUiReal() {
|
||||
updateConversationList();
|
||||
if (conversationList.size() > 0) {
|
||||
if (!this.mConversationFragment.isAdded()) {
|
||||
Log.d(Config.LOGTAG,"fragment NOT added to activity. detached="+Boolean.toString(mConversationFragment.isDetached()));
|
||||
}
|
||||
ConversationActivity.this.mConversationFragment.updateMessages();
|
||||
updateActionBarTitle();
|
||||
invalidateOptionsMenu();
|
||||
} else {
|
||||
Log.d(Config.LOGTAG,"not updating conversations fragment because conversations list size was 0");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,15 +1,33 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.support.v13.view.inputmethod.EditorInfoCompat;
|
||||
import android.support.v13.view.inputmethod.InputConnectionCompat;
|
||||
import android.support.v13.view.inputmethod.InputContentInfoCompat;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.Editable;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Spanned;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.widget.EditText;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
|
||||
public class EditMessage extends EditText {
|
||||
|
||||
public interface OnCommitContentListener {
|
||||
boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] mimeTypes);
|
||||
}
|
||||
|
||||
private OnCommitContentListener mCommitContentListener = null;
|
||||
private String[] mimeTypes = null;
|
||||
|
||||
public EditMessage(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
@ -69,6 +87,7 @@ public class EditMessage extends EditText {
|
|||
this.isUserTyping = false;
|
||||
this.keyboardListener.onTextDeleted();
|
||||
}
|
||||
this.keyboardListener.onTextChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,7 +103,63 @@ public class EditMessage extends EditText {
|
|||
void onTypingStarted();
|
||||
void onTypingStopped();
|
||||
void onTextDeleted();
|
||||
void onTextChanged();
|
||||
boolean onTabPressed(boolean repeated);
|
||||
}
|
||||
|
||||
private static final InputFilter SPAN_FILTER = new InputFilter() {
|
||||
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
|
||||
return source instanceof Spanned ? source.toString() : source;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onTextContextMenuItem(int id) {
|
||||
if (id == android.R.id.paste) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return super.onTextContextMenuItem(android.R.id.pasteAsPlainText);
|
||||
} else {
|
||||
Editable editable = getEditableText();
|
||||
InputFilter[] filters = editable.getFilters();
|
||||
InputFilter[] tempFilters = new InputFilter[filters != null ? filters.length + 1 : 1];
|
||||
if (filters != null) {
|
||||
System.arraycopy(filters, 0, tempFilters, 1, filters.length);
|
||||
}
|
||||
tempFilters[0] = SPAN_FILTER;
|
||||
editable.setFilters(tempFilters);
|
||||
try {
|
||||
return super.onTextContextMenuItem(id);
|
||||
} finally {
|
||||
editable.setFilters(filters);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return super.onTextContextMenuItem(id);
|
||||
}
|
||||
}
|
||||
|
||||
public void setRichContentListener(String[] mimeTypes, OnCommitContentListener listener) {
|
||||
this.mimeTypes = mimeTypes;
|
||||
this.mCommitContentListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
|
||||
final InputConnection ic = super.onCreateInputConnection(editorInfo);
|
||||
|
||||
if (mimeTypes != null && mCommitContentListener != null) {
|
||||
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
|
||||
return InputConnectionCompat.createWrapper(ic, editorInfo, new InputConnectionCompat.OnCommitContentListener() {
|
||||
@Override
|
||||
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
|
||||
return EditMessage.this.mCommitContentListener.onCommitContent(inputContentInfo, flags, opts, mimeTypes);
|
||||
}
|
||||
});
|
||||
}
|
||||
else {
|
||||
return ic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface.OnClickListener;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.AutoCompleteTextView;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
|
@ -47,9 +47,11 @@ public class EnterJidDialog {
|
|||
AlertDialog.Builder builder = new AlertDialog.Builder(context);
|
||||
builder.setTitle(title);
|
||||
View dialogView = LayoutInflater.from(context).inflate(R.layout.enter_jid_dialog, null);
|
||||
final TextView jabberIdDesc = (TextView) dialogView.findViewById(R.id.jabber_id);
|
||||
jabberIdDesc.setText(R.string.account_settings_jabber_id);
|
||||
final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
|
||||
final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid);
|
||||
jid.setAdapter(new KnownHostsAdapter(context,android.R.layout.simple_list_item_1, knownHosts));
|
||||
jid.setAdapter(new KnownHostsAdapter(context, R.layout.simple_list_item, knownHosts));
|
||||
if (prefilledJid != null) {
|
||||
jid.append(prefilledJid);
|
||||
if (!allowEditJid) {
|
||||
|
@ -60,15 +62,16 @@ public class EnterJidDialog {
|
|||
}
|
||||
}
|
||||
|
||||
jid.setHint(R.string.account_settings_example_jabber_id);
|
||||
|
||||
if (account == null) {
|
||||
StartConversationActivity.populateAccountSpinner(context, activatedAccounts, spinner);
|
||||
} else {
|
||||
ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
|
||||
android.R.layout.simple_spinner_item,
|
||||
R.layout.simple_list_item,
|
||||
new String[] { account });
|
||||
spinner.setEnabled(false);
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
adapter.setDropDownViewResource(R.layout.simple_list_item);
|
||||
spinner.setAdapter(adapter);
|
||||
}
|
||||
|
||||
|
@ -118,8 +121,9 @@ public class EnterJidDialog {
|
|||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void show() {
|
||||
public Dialog show() {
|
||||
this.dialog.show();
|
||||
this.dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this.dialogOnClick);
|
||||
return this.dialog;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.preference.Preference;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import eu.siacs.conversations.services.ExportLogsService;
|
||||
|
||||
public class ExportLogsPreference extends Preference {
|
||||
|
||||
public ExportLogsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
public ExportLogsPreference(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public ExportLogsPreference(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
protected void onClick() {
|
||||
final Intent startIntent = new Intent(getContext(), ExportLogsService.class);
|
||||
getContext().startService(startIntent);
|
||||
super.onClick();
|
||||
}
|
||||
}
|
120
src/main/java/eu/siacs/conversations/ui/MagicCreateActivity.java
Normal file
|
@ -0,0 +1,120 @@
|
|||
package eu.siacs.conversations.ui;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import eu.siacs.conversations.Config;
|
||||
import eu.siacs.conversations.R;
|
||||
import eu.siacs.conversations.entities.Account;
|
||||
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
|
||||
import eu.siacs.conversations.xmpp.jid.Jid;
|
||||
|
||||
public class MagicCreateActivity extends XmppActivity implements TextWatcher {
|
||||
|
||||
private TextView mFullJidDisplay;
|
||||
private EditText mUsername;
|
||||
private SecureRandom mRandom;
|
||||
|
||||
private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456780+-/#$!?";
|
||||
private static final int PW_LENGTH = 10;
|
||||
|
||||
@Override
|
||||
protected void refreshUiReal() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
void onBackendConnected() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
if (getResources().getBoolean(R.bool.portrait_only)) {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.magic_create);
|
||||
mFullJidDisplay = (TextView) findViewById(R.id.full_jid);
|
||||
mUsername = (EditText) findViewById(R.id.username);
|
||||
mRandom = new SecureRandom();
|
||||
Button next = (Button) findViewById(R.id.create_account);
|
||||
next.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
String username = mUsername.getText().toString();
|
||||
if (username.contains("@") || username.length() < 3) {
|
||||
mUsername.setError(getString(R.string.invalid_username));
|
||||
mUsername.requestFocus();
|
||||
} else {
|
||||
mUsername.setError(null);
|
||||
try {
|
||||
Jid jid = Jid.fromParts(username.toLowerCase(), Config.MAGIC_CREATE_DOMAIN, null);
|
||||
Account account = xmppConnectionService.findAccountByJid(jid);
|
||||
if (account == null) {
|
||||
account = new Account(jid, createPassword());
|
||||
account.setOption(Account.OPTION_REGISTER, true);
|
||||
account.setOption(Account.OPTION_DISABLED, true);
|
||||
account.setOption(Account.OPTION_MAGIC_CREATE, true);
|
||||
xmppConnectionService.createAccount(account);
|
||||
}
|
||||
Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class);
|
||||
intent.putExtra("jid", account.getJid().toBareJid().toString());
|
||||
intent.putExtra("init", true);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
Toast.makeText(MagicCreateActivity.this, R.string.secure_password_generated, Toast.LENGTH_SHORT).show();
|
||||
startActivity(intent);
|
||||
} catch (InvalidJidException e) {
|
||||
mUsername.setError(getString(R.string.invalid_username));
|
||||
mUsername.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
mUsername.addTextChangedListener(this);
|
||||
}
|
||||
|
||||
private String createPassword() {
|
||||
StringBuilder builder = new StringBuilder(PW_LENGTH);
|
||||
for(int i = 0; i < PW_LENGTH; ++i) {
|
||||
builder.append(CHARS.charAt(mRandom.nextInt(CHARS.length() - 1)));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s.toString().trim().length() > 0) {
|
||||
try {
|
||||
mFullJidDisplay.setVisibility(View.VISIBLE);
|
||||
Jid jid = Jid.fromParts(s.toString().toLowerCase(), Config.MAGIC_CREATE_DOMAIN, null);
|
||||
mFullJidDisplay.setText(getString(R.string.your_full_jid_will_be, jid.toString()));
|
||||
} catch (InvalidJidException e) {
|
||||
mFullJidDisplay.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
} else {
|
||||
mFullJidDisplay.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -102,6 +102,15 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
|||
registerForContextMenu(accountListView);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
final int theme = findTheme();
|
||||
if (this.mTheme != theme) {
|
||||
recreate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(final Bundle savedInstanceState) {
|
||||
if (selectedAccount != null) {
|
||||
|
@ -121,9 +130,11 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
|||
menu.findItem(R.id.mgmt_account_disable).setVisible(false);
|
||||
menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
|
||||
menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
|
||||
menu.findItem(R.id.mgmt_account_change_presence).setVisible(false);
|
||||
} else {
|
||||
menu.findItem(R.id.mgmt_account_enable).setVisible(false);
|
||||
menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(!Config.HIDE_PGP_IN_UI);
|
||||
menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp());
|
||||
menu.findItem(R.id.mgmt_account_change_presence).setVisible(manuallyChangePresence());
|
||||
}
|
||||
menu.setHeaderTitle(this.selectedAccount.getJid().toBareJid().toString());
|
||||
}
|
||||
|
@ -184,6 +195,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
|||
case R.id.mgmt_account_announce_pgp:
|
||||
publishOpenPGPPublicKey(selectedAccount);
|
||||
return true;
|
||||
case R.id.mgmt_account_change_presence:
|
||||
changePresence(selectedAccount);
|
||||
return true;
|
||||
default:
|
||||
return super.onContextItemSelected(item);
|
||||
}
|
||||
|
@ -232,6 +246,12 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
|||
}
|
||||
}
|
||||
|
||||
private void changePresence(Account account) {
|
||||
Intent intent = new Intent(this, SetPresenceActivity.class);
|
||||
intent.putExtra(SetPresenceActivity.EXTRA_ACCOUNT,account.getJid().toBareJid().toString());
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
public void onClickTglAccountState(Account account, boolean enable) {
|
||||
if (enable) {
|
||||
enableAccount(account);
|
||||
|
@ -307,17 +327,21 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
|||
|
||||
private void disableAccount(Account account) {
|
||||
account.setOption(Account.OPTION_DISABLED, true);
|
||||
xmppConnectionService.updateAccount(account);
|
||||
if (!xmppConnectionService.updateAccount(account)) {
|
||||
Toast.makeText(this,R.string.unable_to_update_account,Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void enableAccount(Account account) {
|
||||
account.setOption(Account.OPTION_DISABLED, false);
|
||||
xmppConnectionService.updateAccount(account);
|
||||
if (!xmppConnectionService.updateAccount(account)) {
|
||||
Toast.makeText(this,R.string.unable_to_update_account,Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private void publishOpenPGPPublicKey(Account account) {
|
||||
if (ManageAccountActivity.this.hasPgp()) {
|
||||
choosePgpSignId(selectedAccount);
|
||||
announcePgp(selectedAccount, null, onOpenPGPKeyPublished);
|
||||
} else {
|
||||
this.showInstallPgpDialog();
|
||||
}
|
||||
|
@ -349,12 +373,12 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
|
|||
if (requestCode == REQUEST_CHOOSE_PGP_ID) {
|
||||
if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
|
||||
selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
|
||||
announcePgp(selectedAccount, null);
|
||||
announcePgp(selectedAccount, null, onOpenPGPKeyPublished);
|
||||
} else {
|
||||
choosePgpSignId(selectedAccount);
|
||||
}
|
||||
} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
|
||||
announcePgp(selectedAccount, null);
|
||||
announcePgp(selectedAccount, null, onOpenPGPKeyPublished);
|
||||
}
|
||||
this.mPostponedActivityResult = null;
|
||||
} else {
|
||||
|
|