Compare commits

..

216 commits

Author SHA1 Message Date
Daniel Gultsch
5fd91fcd8e Merge pull request #2272 from mimi89999/patch-3
Correct a typo in the README
2017-01-31 21:45:43 +01:00
Michel Le Bihan
cd3ce76115 Correct a typo in the README 2017-01-31 18:09:42 +01:00
Daniel Gultsch
8a1ebf2bbe Merge pull request #2260 from alexara/patch-1
Fixed typos in the readme
2017-01-26 19:45:29 +01:00
Daniel Gultsch
0d199c8ceb Update README.md 2017-01-26 19:45:06 +01:00
Daniel Gultsch
7651700c2a Merge branch 'master' into patch-1 2017-01-26 19:42:57 +01:00
Daniel Gultsch
eea1bc8090 Merge pull request #2259 from mabkenar/patch-2
small typo fix
2017-01-26 19:40:57 +01:00
Daniel Gultsch
53241f2ef1 add explicit encryption hints to outgoing messages 2017-01-26 19:19:08 +01:00
alexara
e4524e2c7b Fixed typos in the readme 2017-01-26 19:18:22 +01:00
Daniel Gultsch
c9e6d05fa0 use base64 encoding for file names uploaded with http 2017-01-26 18:39:25 +01:00
Masoud Abkenar
be84443921 small typo fix 2017-01-26 16:46:19 +01:00
Daniel Gultsch
bbceee7f61 pulled translation from transifex 2017-01-26 12:35:34 +01:00
Daniel Gultsch
40ee1a0bfc reset messagesLoaded when changing retention settings 2017-01-25 18:35:22 +01:00
Daniel Gultsch
a86b2fefd9 add database and file migrations for 1.16.0 2017-01-25 13:22:20 +01:00
Daniel Gultsch
f2d9539d90 share uri for bookmark direclty from Start Conversation 2017-01-25 00:15:50 +01:00
Daniel Gultsch
66457c9f2e transcode videos before sharing. change storage location 2017-01-24 20:17:36 +01:00
Daniel Gultsch
9b6ae6d75f configurable local message retention period. (untested) 2017-01-23 17:14:30 +01:00
Daniel Gultsch
4c6ef3b24e cleaning up crypto targets when conference member is getting removed 2017-01-22 18:58:49 +01:00
Daniel Gultsch
b48bf39e08 change behaviour of back button to close finish activity. fixes #704 2017-01-22 18:26:47 +01:00
Daniel Gultsch
7035f38e0b Merge branch 'master' of github.com:siacs/Conversations 2017-01-22 13:08:44 +01:00
Daniel Gultsch
d53c813408 make sure to set open conversations after connection with background service 2017-01-22 13:08:21 +01:00
Daniel Gultsch
b72d7ec8d0 make sure to properly stop tagwriter 2017-01-22 12:54:39 +01:00
Daniel Gultsch
5dde977233 upgrade dependency to ShortcutBadger 2017-01-22 12:37:16 +01:00
Daniel Gultsch
2f4eee1fa7 Merge pull request #2230 from AnBuKu/patch-1
Added XEP-0384
2017-01-22 10:41:45 +01:00
Daniel Gultsch
96a6460744 don't quote text when '>' is followed by numeber 2017-01-21 11:07:23 +01:00
Daniel Gultsch
780d1daf7e fixed some issues around ibb 2017-01-20 22:37:50 +01:00
Daniel Gultsch
0f870223c4 version bump to 1.15.5 + changelog 2017-01-20 15:03:01 +01:00
Daniel Gultsch
5faa05ca19 pulled translations from transifex 2017-01-20 15:02:19 +01:00
Daniel Gultsch
97ba0a0d49 write text in bold when highlighted in received muc message 2017-01-20 14:54:59 +01:00
Daniel Gultsch
cb9c4d4327 disable automatic foreground enabler. fixes #2239 2017-01-20 14:21:59 +01:00
Daniel Gultsch
c324f0c8df modified highlight nick behaviour to better work with quotes 2017-01-20 13:45:09 +01:00
Daniel Gultsch
59f82cbd34 fixed regression introduced in previous commit. 2017-01-20 13:44:29 +01:00
Daniel Gultsch
143ad48be1 don't prematurly mark conversation as read during activity start. fixes #2245 2017-01-20 10:43:50 +01:00
Daniel Gultsch
1dcf804618 fixed pgp encrypted text quick sharing. fixes #2237 2017-01-17 15:56:21 +01:00
Daniel Gultsch
ac2eee8e81 Merge pull request #2233 from SamWhited/scram-sha-2
Add SCRAM-SHA-2 support
2017-01-16 13:14:48 +01:00
Daniel Gultsch
764026b87e fixed behaviour with non-default encryption masks 2017-01-16 13:09:36 +01:00
Daniel Gultsch
7219e077f4 version bump to 1.15.4 + changelog 2017-01-16 12:07:50 +01:00
Sam Whited
bfc2cffc2f Add SCRAM-SHA-2 support 2017-01-15 23:43:44 -06:00
Daniel Gultsch
f7c5a5c42e pulled translations from transifex 2017-01-15 19:22:18 +01:00
Daniel Gultsch
9bdd2bf1ae Merge branch 'master' of github.com:siacs/Conversations 2017-01-15 18:55:15 +01:00
Daniel Gultsch
d028f4b398 refactored whispermessage processing 2017-01-15 18:54:47 +01:00
Daniel Gultsch
b085426d22 fixed linkifier 2017-01-15 18:54:15 +01:00
Daniel Gultsch
a71e3d0653 pulled translations from transifex 2017-01-14 18:10:18 +01:00
Daniel Gultsch
8f39a594ff partially improved logging for receiving omemo messages 2017-01-14 18:10:04 +01:00
Daniel Gultsch
ebf8ae231a fixed subheading of domain hosting faq entry 2017-01-13 12:14:27 +01:00
Daniel Gultsch
aa7bfe9fe7 update readme to refer to domain hosting 2017-01-13 12:12:09 +01:00
Daniel Gultsch
b2e9b4aeb1 pulled translations from transifex 2017-01-12 23:28:30 +01:00
Daniel Gultsch
8e025cbb9e show doze warning when push is running on prosody 2017-01-12 23:22:02 +01:00
Daniel Gultsch
1876b444fa refactor getServerIdentity() to parse disco result directly 2017-01-12 23:17:52 +01:00
Daniel Gultsch
c03e3b5965 don't include 'before' reference in mam queries bound by timestamp 2017-01-12 20:56:55 +01:00
Daniel Gultsch
fd7216b6a0 finish of backlog only for one particular account 2017-01-12 20:56:27 +01:00
Daniel Gultsch
585a538340 don't show key tile in contact details when there are no keys 2017-01-12 20:50:53 +01:00
Daniel Gultsch
b050ff2576 only call UI thread from downloading thread every 250ms 2017-01-12 16:02:09 +01:00
Daniel Gultsch
bfacc180c5 don't allow to purge keys. offer distrut instead 2017-01-12 15:59:13 +01:00
Daniel Gultsch
2c1d3ef968 fixed avatar republish missing the mime type 2017-01-12 12:20:10 +01:00
AnBuKu
313baca84e Added XEP-0384
Maybe style [XEP-0384: OMEMO Encryption](http://xmpp.org/extensions/xep-0384.html) would be better for user's convenience; of course for other XEP's as well
2017-01-10 15:18:01 +01:00
Daniel Gultsch
f0c3b31a42 treat omemo keys >= 32 bytes as containing auth tag. add config flag to put auth tag in key 2017-01-09 21:47:07 +01:00
Daniel Gultsch
a1cb855739 adding prekey='true' to omemo messages if applicable 2017-01-09 20:20:02 +01:00
Daniel Gultsch
ef4ed90811 pulled translations from transifex 2017-01-09 19:54:44 +01:00
Daniel Gultsch
39bb8ad05f automatically bookmark private, non-anonymous mucs where inviter is trusted. fixes #2035 #937 2017-01-09 19:54:27 +01:00
Daniel Gultsch
b09b8136d2 version bump to 1.15.3 + changelog 2017-01-09 19:52:46 +01:00
Daniel Gultsch
a994d8f847 fixed typo in variable name 2017-01-09 18:05:58 +01:00
Daniel Gultsch
b19572ba8c use 7.1 web url pattern matching on old platforms as well. fixes #2228 2017-01-09 17:58:11 +01:00
Daniel Gultsch
d192c529e0 add spaces to otr fingerprints copied to clipboard. fixes #2226 2017-01-09 17:57:37 +01:00
Daniel Gultsch
b116926bb1 unify getFileUri across share and open intents 2017-01-09 17:00:08 +01:00
Daniel Gultsch
39c8867ed7 add more punctuations to message preview 2017-01-06 20:56:44 +01:00
Daniel Gultsch
1269123816 Merge pull request #2224 from illegalprime/fix-travis-android-25
Updated travis and Trust Manager to fix build
2017-01-06 20:46:27 +01:00
Michael Eden
cd772360db updated travis and trust manager 2017-01-06 14:28:57 -05:00
Daniel Gultsch
4a299920dc add overlay to indicate that image is gif 2017-01-03 14:05:10 +01:00
Daniel Gultsch
e6ba8484fa update build tools and some dependencies 2017-01-03 12:33:46 +01:00
Daniel Gultsch
470d244414 Merge branch 'feature-gboardgifs' of https://github.com/illegalprime/Conversations into illegalprime-feature-gboardgifs 2017-01-03 11:44:14 +01:00
Daniel Gultsch
2bb7bc1455 show offline contacts as grayed out in conference details 2017-01-03 11:40:29 +01:00
Michael Eden
5a670c88b0 Do not compress GIFs, allow GBoard to send GIFs 2017-01-01 16:16:35 -05:00
Daniel Gultsch
fa70bd7536 disable automatic foreground service activation if related config paramaters are set to zero 2016-12-30 20:24:35 +01:00
Daniel Gultsch
b8b2051f4c get rid of unecessary config debug paramater that has been replaced by exepert setting 2016-12-30 20:23:50 +01:00
Daniel Gultsch
8c34bb3c6f hide inactive devices by default in contact details 2016-12-30 13:17:45 +01:00
Daniel Gultsch
40a9f70478 always open account details when scanning one of our own keys. fixes #2211 2016-12-29 12:50:18 +01:00
Daniel Gultsch
fcd9ab17fe don't throw assertion error when building session with same device id from other contact 2016-12-28 22:15:24 +01:00
Daniel Gultsch
b8f67bfaa3 deduplicate corrected messages 2016-12-26 15:13:38 +01:00
Daniel Gultsch
82c2e89d21 stop using broken parallax distance in sliding pane layout 2016-12-25 18:57:30 +01:00
Daniel Gultsch
593dd259a9 version bump to 1.15.2 + changelog 2016-12-23 21:27:27 +01:00
Daniel Gultsch
c43f224e8b pulled translations from transifex 2016-12-23 21:22:16 +01:00
Daniel Gultsch
9972f5eabc fixed npe cause by race condition when axolotl service isn't initialized 2016-12-23 21:19:38 +01:00
Daniel Gultsch
28c64c2bd1 skip empty lines in message preview. prevents indexoutofbounds exception 2016-12-23 21:19:11 +01:00
Daniel Gultsch
d03c431137 use original message to parse pep 2016-12-23 21:16:58 +01:00
Daniel Gultsch
6c10f8a232 version bump to 1.15.1+ changelog 2016-12-21 14:04:56 +01:00
Daniel Gultsch
f77afd9596 pulled translations from transifex 2016-12-20 16:38:07 +01:00
Daniel Gultsch
b011d46ff2 don't show quoted text in message preview 2016-12-20 16:35:08 +01:00
Daniel Gultsch
e5fff42b10 added omemo padding but disabled by Config.java flag 2016-12-20 16:12:12 +01:00
Daniel Gultsch
fbbf1a37b4 disable removing of broken devices by default 2016-12-18 11:49:27 +01:00
Daniel Gultsch
dbda2afd6d remove broken devices only once to prevent loops 2016-12-18 11:47:42 +01:00
Daniel Gultsch
87746ca2ba remove own fetch errors from device announcement 2016-12-16 17:12:26 +01:00
Daniel Gultsch
da914ba09c make sure to display encryption indicatior 2016-12-16 11:30:51 +01:00
Daniel Gultsch
75ee14cfdf don't reconnect accout when system reports no internet connection 2016-12-10 13:20:05 +01:00
Daniel Gultsch
55b60f6b0f don't correct a message if that would create a duplicate 2016-12-09 20:03:48 +01:00
Daniel Gultsch
88321c1e8c use POSH only when system CAs are trusted 2016-12-09 19:56:49 +01:00
Daniel Gultsch
8abfbf82fa use verified symbol instead of colored lock icons 2016-12-09 18:46:32 +01:00
Daniel Gultsch
8d127f70d0 follow redirects in posh 2016-12-08 14:21:15 +01:00
Daniel Gultsch
8eb292d16a don't show unavailable quick actions in settings 2016-12-06 23:44:39 +01:00
Daniel Gultsch
1739af2a41 fixed http resume 2016-12-06 23:27:29 +01:00
Daniel Gultsch
b879fb3753 don't use posh for IPs and set a 5s timeout 2016-12-06 12:23:40 +01:00
Daniel Gultsch
cbc9c1fb20 add support for RFC7711 to MTM 2016-12-05 21:52:44 +01:00
Daniel Gultsch
1e7b4030bb show jid monospaced in verify dialog 2016-12-04 13:39:08 +01:00
Daniel Gultsch
1a89915b31 disable 'show blocklist' if blocklist is empty. fixes #2164 2016-12-03 23:49:00 +01:00
Daniel Gultsch
a5b3c579c4 redraw options menu after rotation in muc details. fixes #2161 2016-12-03 23:25:31 +01:00
Daniel Gultsch
56991bbaeb add omemo fingerprints to web links as well 2016-12-03 13:37:26 +01:00
Daniel Gultsch
6e289b8738 show warning dialog beforing verifying keys via a link 2016-12-03 13:19:56 +01:00
Daniel Gultsch
599f7dad2c Merge branch 'feature-quotation' of https://github.com/Mishiranu/Conversations into Mishiranu-feature-quotation 2016-12-02 14:01:26 +01:00
Daniel Gultsch
d4b1119240 default using internal storage to false 2016-12-02 11:35:00 +01:00
Daniel Gultsch
6b0242523b Merge branch 'master' of https://github.com/Fenisu/Conversations into Fenisu-master 2016-12-02 11:25:14 +01:00
Daniel Gultsch
5d4aa04e5d support for jid escapting when displaying localpart only 2016-12-01 20:49:18 +01:00
Daniel Gultsch
58de10bcab use prepped string when building axolotl session 2016-12-01 20:48:39 +01:00
Daniel Gultsch
e127ba9361 don't use own jid joined from another client to generate muc title 2016-12-01 19:57:40 +01:00
Daniel Gultsch
6e95ad4bdf don't show share button before account is setup 2016-12-01 13:07:18 +01:00
Daniel Gultsch
168ad50ddd only show contact related snackbars when conversation is single 2016-12-01 12:50:40 +01:00
Daniel Gultsch
f0f2aab92d made provider authorities relativ to deal with different package ids 2016-12-01 12:09:49 +01:00
Daniel Gultsch
96a992353b avoid binding multiple times from BarcodeService 2016-12-01 11:34:04 +01:00
Daniel Gultsch
c62f3f99be increment version code and release version 1.15.0 2016-11-30 10:48:30 +01:00
Daniel Gultsch
a7ec23ef30 pulled translations from transifex 2016-11-30 10:47:25 +01:00
Daniel Gultsch
1b9a91eb2f renamed foreground service preference 2016-11-30 10:45:39 +01:00
Daniel Gultsch
9d744add38 pulled translations from transifex 2016-11-29 13:51:34 +01:00
Daniel Gultsch
9e7a54849d better handle the case when same user is joined with multiple nicks in the same room 2016-11-29 13:43:52 +01:00
Daniel Gultsch
33e6d8a1ce pulled translations from transifex 2016-11-28 15:51:52 +01:00
Daniel Gultsch
e5d7357e6e mark conversations as read after receiving blocklist push for that conversations 2016-11-28 15:51:11 +01:00
Daniel Gultsch
84a2fa0041 allow fingerprint verification via context menu 2016-11-28 15:11:44 +01:00
Daniel Gultsch
bbe01c9a6a add support for body paramater in xmpp uri 2016-11-28 15:09:02 +01:00
Daniel Gultsch
fb6f0649c3 sent messages from unverified devices show red lock 2016-11-28 15:08:33 +01:00
Daniel Gultsch
fdf19ae287 pulled translations from transifex 2016-11-25 17:06:23 +01:00
Daniel Gultsch
d983f0bc71 fixed migrations from pre-btbv phase 2016-11-25 17:04:23 +01:00
Daniel Gultsch
22ca8200fa Merge branch 'master' of github.com:siacs/Conversations 2016-11-25 16:58:37 +01:00
Daniel Gultsch
988cce6320 Merge pull request #2147 from licaon-kter/patch-5
Typo in Readme
2016-11-25 16:57:43 +01:00
Mishiranu
f4a769080b Add quotation support 2016-11-25 17:06:43 +03:00
Daniel Gultsch
f36dff485e changed blind trust before verification summary to a slightly longer one 2016-11-24 19:59:57 +01:00
licaon-kter
6320a3ca7c Typo in Readme 2016-11-24 18:35:10 +02:00
Daniel Gultsch
43fd5e5fe6 Merge pull request #2146 from licaon-kter/patch-4
Add FAQ about the new trust concept
2016-11-24 15:39:51 +01:00
licaon-kter
84a4f4d66d Add FAQ about the new trust concept 2016-11-24 15:17:02 +02:00
Daniel Gultsch
a87f7903c6 always force close a connection when disabling from error state 2016-11-24 12:44:24 +01:00
Daniel Gultsch
1e59a9517a bumped gradle to 1.15.0-beta 2016-11-24 12:06:15 +01:00
Daniel Gultsch
6a5d2e35b5 pulled translations from transifex 2016-11-24 12:05:52 +01:00
Daniel Gultsch
cbd45d3ee5 changed design language to match BTBV proposal
* untrusted messages have red background
* unverified message have normal background and red lock
2016-11-24 11:29:26 +01:00
Daniel Gultsch
2ec7165381 update the conversations view (and the lock icon) after receiving device list 2016-11-24 11:28:04 +01:00
Daniel Gultsch
20d3a41b52 explictly scan for aztec and qr codes only 2016-11-23 11:01:58 +01:00
Daniel Gultsch
839ef8e14b introduced blind trust before verification mode
read more about the concept on https://gultsch.de/trust.html
2016-11-23 10:42:27 +01:00
Daniel Gultsch
4720ac94d3 Merge branch 'master' of github.com:siacs/Conversations 2016-11-22 22:32:05 +01:00
Daniel Gultsch
07fe434cc7 added share button to account details 2016-11-22 22:31:46 +01:00
Daniel Gultsch
d2268c6a6f show proper avatar for 'self' contact. fixes #2138 2016-11-22 12:34:16 +01:00
Daniel Gultsch
d76b0a3104 offer verification directly from the trust keys screen 2016-11-22 12:03:21 +01:00
Daniel Gultsch
1a7e0fd153 use aztec code instead of qr 2016-11-21 12:01:01 +01:00
Daniel Gultsch
6631705aea use constants for some preferences 2016-11-21 11:03:38 +01:00
Daniel Gultsch
7b99346a4b when swiping don't clean startup counter entirely. just don't count last startup 2016-11-21 10:48:59 +01:00
Daniel Gultsch
1c31b96920 Merge pull request #2130 from da2x/patch-2
Fix up the langauge in some Settings strings
2016-11-20 00:39:57 +01:00
Daniel Gultsch
568d6c8392 Merge branch 'master' of github.com:siacs/Conversations 2016-11-20 00:39:25 +01:00
Daniel Gultsch
64e8035f6d introduced custom tls socket factory to make tls1.2 work for http connections 2016-11-20 00:39:01 +01:00
Daniel Gultsch
b71aa6d3a4 remove omemo devices from annoucement after 7 days of inactivity 2016-11-19 21:39:16 +01:00
Daniel Gultsch
2614706d39 don't show omemo keys by default in account details 2016-11-19 21:32:40 +01:00
Daniel Gultsch
cb639f3fdd don't use xmpp uri for self verification if account is disabled 2016-11-19 21:31:41 +01:00
Daniel Gultsch
6362799d56 save last activation time in fingerprint status 2016-11-19 13:34:54 +01:00
Daniel Gultsch
40c747660d removed some unecessary locking 2016-11-19 13:34:27 +01:00
Daniel Gultsch
8132480b82 close socket after failed stream open 2016-11-19 12:20:31 +01:00
Daniel Gultsch
3bf2876e09 check if thread was interrupted before doing operations on socket 2016-11-19 10:44:40 +01:00
Daniel Gultsch
1820b163a1 fixed regression that would crash create contact dialog. fixes #2131 2016-11-19 10:29:08 +01:00
Daniel Aleksandersen
965f73f95a Fix up the langauge in some Settings strings 2016-11-19 05:00:16 +01:00
Daniel Gultsch
2b9b3be3f1 show 'clear devices' button underneath own devices 2016-11-18 21:49:52 +01:00
Daniel Gultsch
a86a36f570 removed some unecessary logging from omemo message generation 2016-11-18 20:13:09 +01:00
Daniel Gultsch
01f92ef4ee lower own otr fingerprint 2016-11-18 20:12:45 +01:00
Daniel Gultsch
d68b7cfcfc issue ping after network change 2016-11-18 14:00:05 +01:00
Daniel Gultsch
fef601b4ae lower reconnection time 2016-11-18 13:58:01 +01:00
Daniel Gultsch
0303c28ad9 synchronzie on xmpp service around all state changes 2016-11-18 13:58:01 +01:00
Daniel Gultsch
1ed2445c1d don't reset last connect time on network change 2016-11-18 13:55:02 +01:00
Daniel Gultsch
a7ee8f8a74 use lower case otr fingerprints for comparison 2016-11-18 13:13:29 +01:00
Daniel Gultsch
9d9a9e63ad removed some very verbose logging from axolotl service 2016-11-18 13:03:02 +01:00
Daniel Gultsch
99a41265b8 lower casing fingerprints when parsing URI 2016-11-18 13:02:33 +01:00
Daniel Gultsch
7ec38bd202 added section to FAQ about default encryption 2016-11-18 12:03:02 +01:00
Daniel Gultsch
211354ee26 put omemo fingerprint in own uri (qr code / nfc) 2016-11-17 22:28:45 +01:00
Daniel Gultsch
7e2e42cb11 parse omemo fingerprints from uris 2016-11-17 20:09:42 +01:00
Daniel Gultsch
3f3b360eee fixed back and forth between Welcome- and EditAccountActivity 2016-11-17 11:40:29 +01:00
Daniel Gultsch
ad9a8c2281 use base64.nowrap for omemo keys 2016-11-17 10:58:44 +01:00
Daniel Gultsch
4d965e96ed reset startup count when swiped away (only count kills) 2016-11-17 10:58:26 +01:00
Daniel Gultsch
5007aa1b07 update shortcut badger 2016-11-16 14:03:25 +01:00
Daniel Gultsch
d8bff08f1f slightly darken verified icon + mark inactive 2016-11-16 09:39:44 +01:00
Daniel Gultsch
ec63900ef3 work around -1 in next encryption 2016-11-15 21:11:35 +01:00
Daniel Gultsch
48afeb571b refactor omemo fingerprint UI code 2016-11-15 20:00:52 +01:00
Daniel Gultsch
e84af51272 distinguish between general i/o error and write exception when copying files 2016-11-15 15:43:04 +01:00
Daniel Gultsch
d61b00604d fixed enabling trust toggle. unknown->untrusted 2016-11-15 15:14:21 +01:00
Daniel Gultsch
05fc15be3d refactore trust enum to be FingerprintStatus class with trust and active 2016-11-14 22:27:41 +01:00
Daniel Gultsch
6da8b50d95 increase restart threshold 2016-11-14 19:49:17 +01:00
Daniel Gultsch
a753e28ad2 pulled ru translation from transifex 2016-11-13 19:26:27 +01:00
Daniel Gultsch
1d3167b520 extract affiliations from unavailable presence 2016-11-13 19:25:58 +01:00
Daniel Gultsch
035d0c7957 Stop automagically select default encryption
Selecting a default encryption (in our case OMEMO) has several down sides.
First of all users might have perfectly valid reasons not to use encryption
at all such as using the same private server. Second of all the way it was
implemented Conversations would automatically fall back to plain text as soon
as the conditions changed (recipient switches to device with no encryption)
which lead to unexpected situations.
Thirdly having a default encryptions speaks against the 'mission
statement' of Conversations of not forcing its security and privacey
aspects upon the user.
And last but not least the goal of implementing this feature in the
first place: Be encrypted by default didn't work at all. I don't think
there was a single user that we succesfully 'tricked' into using OMEMO
who otherwise wouldn't have used it.
2016-11-13 17:11:13 +01:00
Daniel Gultsch
bec048407a offer message correction in private convs 2016-11-12 20:25:02 +01:00
Daniel Gultsch
fe62ef32ae don't add outcasts or non-members in members-only rooms back to list 2016-11-12 20:21:11 +01:00
Daniel Gultsch
f7c2cd4807 pulled translations from transifex 2016-11-11 15:01:31 +01:00
Daniel Gultsch
e8cc959a7f don't offer message correction in anonymous mucs 2016-11-11 15:01:15 +01:00
Daniel Gultsch
bd578c59bf version bump to 1.14.9 + changelog 2016-11-08 21:38:12 +01:00
Daniel Gultsch
bb4952c89e pulled translations from transifex 2016-11-08 21:37:59 +01:00
Daniel Gultsch
698ddadbee brought restart threshold down to 8 times in 8h 2016-11-08 21:37:44 +01:00
Daniel Gultsch
1ef8d0a746 don't mark previous conversation as read when processing pending intent. fixes #2079 2016-11-08 12:42:13 +01:00
Daniel Gultsch
bca8f11c9c add frequent restart detection 2016-11-08 12:20:07 +01:00
Ignacio Quezada
297c0a792f Private files using a boolean flag from Config.java. 2016-11-08 11:45:20 +01:00
Daniel Gultsch
1a57599da2 lower case incoming dns records 2016-11-08 10:14:34 +01:00
Daniel Gultsch
00b3d5ee35 Merge branch 'master' of github.com:siacs/Conversations 2016-11-08 10:08:58 +01:00
Daniel Gultsch
b3c19f039c Merge pull request #2108 from licaon-kter/patch-2
Fix typo
2016-11-08 10:08:48 +01:00
licaon-kter
d341904c4d Fix typo 2016-11-08 01:46:46 +02:00
Daniel Gultsch
7978fd768e fixed regression of showing delivery failed after receipt 2016-11-07 21:57:08 +01:00
Daniel Gultsch
b390908610 Merge pull request #2105 from ReadmeCritic/master
Fix typos in README
2016-11-07 17:58:47 +01:00
ReadmeCritic
64ad93dad6 Fix typos in README 2016-11-07 08:46:35 -08:00
Daniel Gultsch
9edbddd7e1 show warning in account details when data saver is enabled 2016-11-07 10:49:43 +01:00
Daniel Gultsch
d369ec767f expanded section on adb in readme 2016-11-02 15:21:45 +01:00
Daniel Gultsch
2c004857f6 handle file attachment when missing connection 2016-11-02 15:21:26 +01:00
Daniel Gultsch
544c5b4a21 removed unnecessary push_mode 2016-11-02 11:04:33 +01:00
Daniel Gultsch
e582b9fc10 leaving low ping timeout mode after coming online 2016-11-02 09:36:14 +01:00
Daniel Gultsch
e538272417 version bump to 1.14.8 + changelog 2016-11-01 10:27:19 +01:00
Daniel Gultsch
20ddba2aa9 fixed npe when jingle partner is using unknown candidate 2016-11-01 10:27:01 +01:00
Daniel Gultsch
07a71d312a extracting stanza-id where by=account 2016-10-31 12:07:08 +01:00
Daniel Gultsch
a5181b22e0 always use ipv4 localhost when using orbot http proxy 2016-10-31 09:53:14 +01:00
Daniel Gultsch
ffebb4677a Revert "use file provider on android M as well"
This reverts commit a4020e85f6.
2016-10-30 20:27:39 +01:00
Daniel Gultsch
a44f35ed69 schedule correct wakeup call when in low ping timeout mode 2016-10-29 21:45:01 +02:00
221 changed files with 7744 additions and 2515 deletions

View file

@ -1,11 +1,12 @@
language: android
jdk:
- oraclejdk8
android:
components:
- platform-tools
- tools
- build-tools-23.0.3
- build-tools-19.1.0
- android-24
- build-tools-25.0.2
- android-25
- extra-android-m2repository
- extra-google-m2repository
- extra-google-google_play_services

View file

@ -1,5 +1,40 @@
###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

View file

@ -120,8 +120,13 @@ My Bitcoin Address is: `1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu`
#### 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 dont 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.
##### 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 have a server somewhere and are willing to put some work in, the best alternative-in the spirit of federation-is to run your own. We recommand 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.
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.
@ -245,7 +250,7 @@ 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 succesful restore. Otherwise OMEMO might not work afterwards.
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
@ -293,7 +298,7 @@ To use OpenPGP you have to install the open source app
manage accounts and choose renew PGP announcement from the contextual menu.
#### OMEMO is grayed out. What do I do?
OMEMO has two requirments: Your server and the server of your contact need to support PEP. Both of you can verify that indivually by opening your account details and selecting ```Server info``` from the menu. The appearing table should list PEP as available. The second requirement is mutal presence subscription. You can verify that be going into the contact details and see if the both check boxes *Send presence updates* and *Receive presence updates* are checked.
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?
@ -320,7 +325,7 @@ is disabled.
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 ensurethat.
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
@ -329,12 +334,21 @@ 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 wont 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 recommand that you use [Gajim](https://gajim.org). You need to install the plugins `OMEMO`, `HTTP Upload` and `URL image preview` to get the best compatibiltiy with Conversations. Plugins can be installed from withhin the app.
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
Unfortunatly we dont have a recommandation 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.
Unfortunately we dont 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
@ -383,12 +397,23 @@ 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

View 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

68
art/play_gif.svg Normal file
View 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

View file

@ -13,7 +13,8 @@ resolutions = {
images = {
'ic_launcher.svg' => ['ic_launcher', 48],
'main_logo.svg' => ['main_logo', 200],
'play_video.svg' => ['play_video', 96],
'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],
@ -50,6 +51,7 @@ images = {
'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],
@ -57,7 +59,7 @@ images = {
'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_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],

View file

@ -23,10 +23,10 @@ configurations {
dependencies {
compile project(':libs:MemorizingTrustManager')
playstoreCompile 'com.google.android.gms:play-services-gcm:9.4.0'
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:24.2.0'
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'
@ -35,11 +35,12 @@ dependencies {
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.4@aar'
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 {
@ -49,14 +50,14 @@ ext {
}
android {
compileSdkVersion 24
buildToolsVersion "23.0.3"
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 14
targetSdkVersion 24
versionCode 180
versionName "1.14.7"
targetSdkVersion 25
versionCode 193
versionName "1.15.5"
archivesBaseName += "-$versionName"
applicationId "eu.siacs.conversations"
}

View file

@ -29,3 +29,4 @@
* XEP-0363: HTTP File Upload
* XEP-0368: SRV records for XMPP over TLS
* XEP-0377: Spam Reporting
* XEP-0384: OMEMO Encryption

View file

@ -7,14 +7,14 @@ buildscript {
}
}
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 {

View file

@ -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);
}
}
});
@ -708,22 +839,39 @@ public class MemorizingTrustManager implements X509TrustManager {
}
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();
}
}
}

View file

@ -21,4 +21,8 @@ public class PushManagementService {
public boolean isStub() {
return true;
}
public boolean availableAndUseful(Account account) {
return false;
}
}

View file

@ -19,6 +19,8 @@
android:name="android.permission.READ_PHONE_STATE"
tools:node="remove" />
<uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
@ -197,13 +199,18 @@
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="eu.siacs.conversations.files"
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>

View file

@ -61,11 +61,6 @@ public final class Config {
public static final int CONNECT_DISCO_TIMEOUT = 20;
public static final int MINI_GRACE_PERIOD = 750;
public static final boolean PUSH_MODE = false; //closes the tcp connection when going to background
//and after PING_MIN_INTERVAL of inactivity
//very experimental. only enable this if you want
//to around with GCM push
public static final int AVATAR_SIZE = 192;
public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
@ -83,6 +78,14 @@ public final class Config {
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
@ -94,21 +97,25 @@ public final class Config {
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 boolean PARSE_REAL_JID_FROM_MUC_MAM = false; //dangerous if server doesnt filter
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
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",

View file

@ -194,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);
}

View file

@ -25,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;
@ -73,6 +77,8 @@ 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 (Config.supportOmemo()
@ -88,7 +94,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
for(Jid jid : jids) {
if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) {
AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId);
if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
return true;
}
@ -98,6 +104,23 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
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();
@ -164,7 +187,6 @@ 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", ""));
@ -200,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) {
@ -213,6 +235,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
SUCCESS,
SUCCESS_VERIFIED,
TIMEOUT,
SUCCESS_TRUSTED,
ERROR
}
@ -256,18 +279,18 @@ 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().toPreppedString(), trust);
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status) {
return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), status);
}
public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Jid jid) {
return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), trust);
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, Jid jid) {
return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), status);
}
public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, List<Jid> jids) {
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, List<Jid> jids) {
Set<IdentityKey> keys = new HashSet<>();
for(Jid jid : jids) {
keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), trust));
keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status));
}
return keys;
}
@ -289,14 +312,20 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
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;
}
private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
@ -307,22 +336,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return sessions;
}
public Set<String> getFingerprintsForOwnSessions() {
Set<String> fingerprints = new HashSet<>();
for (XmppAxolotlSession session : findOwnSessions()) {
fingerprints.add(session.getFingerprint());
}
return fingerprints;
}
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(Jid jid) {
return sessions.hasAny(getAddressForJid(jid));
}
@ -351,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) {
public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
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
&& session.getTrust() == from) {
session.setTrust(to);
if (session != null && session.getFingerprint() != null) {
if (session.getTrust().isActive()) {
session.setTrust(session.getTrust().toInactive());
}
}
}
}
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 attempts and pepBroken status.");
pepBroken = false;
numPublishTriesOnEmptyPep = 0;
Set<Integer> newDevices = new HashSet<>(deviceIds);
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 (deviceIds.contains(getOwnDeviceId())) {
deviceIds.remove(getOwnDeviceId());
} else {
publishOwnDeviceId(deviceIds);
}
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) {
buildSessionFromPEP(ownDeviceAddress);
FetchStatus status = fetchStatusMap.get(ownDeviceAddress);
if (status == null || status == FetchStatus.TIMEOUT) {
fetchStatusMap.put(ownDeviceAddress, FetchStatus.PENDING);
this.buildSessionFromPEP(ownDeviceAddress);
}
}
}
if (needsPublishing) {
publishOwnDeviceId(deviceIds);
}
}
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString()));
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);
this.deviceIds.put(jid, deviceIds);
mXmppConnectionService.updateConversationUi(); //update the lock icon
mXmppConnectionService.keyStatusUpdated(null);
}
@ -419,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() {
@ -445,42 +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.ERROR) {
pepBroken = true;
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,
@ -692,16 +726,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return jids;
}
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
return axolotlStore.getFingerprintTrust(fingerprint);
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) {
@ -724,7 +758,7 @@ 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]);
@ -758,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().toPreppedString(), 0);
if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
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?!");
}
@ -823,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) {
@ -850,7 +912,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
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.toString(), foreignId);
AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId);
if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) {
@ -921,8 +983,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
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;
@ -947,46 +1009,39 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
@Nullable
private XmppAxolotlMessage buildHeader(Conversation conversation) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
account.getJid().toBareJid(), getOwnDeviceId());
private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation conversation) {
Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
Set<XmppAxolotlSession> ownSessions = findOwnSessions();
Collection<XmppAxolotlSession> ownSessions = findOwnSessions();
if (remoteSessions.isEmpty()) {
return null;
return false;
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
for (XmppAxolotlSession session : remoteSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
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.getConversation());
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;
@ -1013,8 +1068,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
executor.execute(new Runnable() {
@Override
public void run() {
XmppAxolotlMessage axolotlMessage = buildHeader(conversation);
onMessageCreatedCallback.run(axolotlMessage);
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
if (buildHeader(axolotlMessage,conversation)) {
onMessageCreatedCallback.run(axolotlMessage);
} else {
onMessageCreatedCallback.run(null);
}
}
});
}
@ -1038,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) {
@ -1063,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) {
@ -1077,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);

View file

@ -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);
}

View file

@ -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
}
}

View file

@ -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);
}
};
@ -185,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);
}
}
@ -208,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);
}
@ -225,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) {
@ -428,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());
}
}

View file

@ -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;
@ -104,7 +106,8 @@ public class XmppAxolotlMessage {
try {
Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
this.keys.put(recipientId, key);
boolean isPreKey =keyElement.getAttributeAsBoolean("prekey");
this.keys.put(recipientId, new XmppAxolotlSession.AxolotlKey(key,isPreKey));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid remote id");
}
@ -162,7 +165,15 @@ public class XmppAxolotlMessage {
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
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) {
@ -170,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;
}
@ -183,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);
}
@ -201,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 {
@ -232,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);
@ -239,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

View file

@ -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,79 +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);
if (!message.getPreKeyId().isPresent()) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage did not contain a PreKeyId");
break;
}
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);
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;
}
}
}

View file

@ -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;
}
}

View file

@ -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,156 +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:
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);
}
}
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;
}
}

View file

@ -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";
}
}

View file

@ -15,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.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;
@ -337,6 +341,10 @@ public class Account extends AbstractEntity {
}
}
public State getTrueStatus() {
return this.status;
}
public void setStatus(final State status) {
this.status = status;
}
@ -486,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;
@ -599,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()));

View file

@ -121,7 +121,7 @@ public class Contact implements ListItem, Blockable {
} else if (this.presenceName != null && mutualPresenceSubscription()) {
return this.presenceName;
} else if (jid.hasLocalpart()) {
return jid.getLocalpart();
return jid.getUnescapedLocalpart();
} else {
return jid.getDomainpart();
}
@ -301,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));
}
}
}

View file

@ -20,6 +20,9 @@ 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;
@ -89,6 +92,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
private String mLastReceivedOtrMessageId = null;
private String mFirstMamReference = null;
private Message correctingMessage;
public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
public boolean hasMessagesLeftOnServer() {
return messagesLeftOnServer;
@ -463,7 +467,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
if (generatedName != null) {
return generatedName;
} else {
return getJid().getLocalpart();
return getJid().getUnescapedLocalpart();
}
}
} else {
@ -627,7 +631,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
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;
}
@ -679,54 +683,41 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
return this.nextCounterpart;
}
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() {
return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
}
public int getNextEncryption() {
final AxolotlService axolotlService = getAccount().getAxolotlService();
int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
if (next == -1) {
if (Config.supportOmemo()
&& axolotlService != null
&& mode == MODE_SINGLE
&& axolotlService.isConversationAxolotlCapable(this)
&& getAccount().getSelfContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)
&& getContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)) {
return Message.ENCRYPTION_AXOLOTL;
} else {
next = this.getMostRecentlyUsedIncomingEncryption();
}
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.supportUnencrypted() && next <= 0) {
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()
&& (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
return Message.ENCRYPTION_PGP;
}
} else if (next == Message.ENCRYPTION_AXOLOTL
&& (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
next = Message.ENCRYPTION_NONE;
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) {
@ -942,6 +933,17 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
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() {
synchronized (this.messages) {
Collections.sort(this.messages, new Comparator<Message>() {

View file

@ -8,6 +8,7 @@ import java.net.MalformedURLException;
import java.net.URL;
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;
@ -410,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
&& CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches()));
&& 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;
}
@ -492,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()))
);
}
@ -811,8 +815,8 @@ public class Message extends AbstractEntity {
}
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() {

View file

@ -3,7 +3,6 @@ package eu.siacs.conversations.entities;
import android.annotation.SuppressLint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@ -395,10 +394,20 @@ public class MucOptions {
if (user != null) {
synchronized (users) {
users.remove(user);
if (membersOnly() &&
nonanonymous() &&
user.affiliation.ranks(Affiliation.MEMBER) &&
user.realJid != null) {
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;
@ -409,7 +418,7 @@ public class MucOptions {
return user;
}
public void addUser(User user) {
public void updateUser(User user) {
User old;
if (user.fullJid == null && user.realJid != null) {
old = findUserByRealJid(user.realJid);
@ -435,7 +444,10 @@ public class MucOptions {
if (old != null) {
users.remove(old);
}
this.users.add(user);
if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
&& user.getAffiliation().outranks(Affiliation.OUTCAST)){
this.users.add(user);
}
}
}
@ -505,8 +517,20 @@ public class MucOptions {
}
public List<User> getUsers(int max) {
ArrayList<User> users = getUsers();
return users.subList(0, Math.min(max, users.size()));
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 subset;
}
public int getUserCount() {

View file

@ -10,12 +10,15 @@ 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;
@ -315,7 +318,7 @@ public class IqGenerator extends AbstractGenerator {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD);
request.addChild("filename").setContent(file.getName());
request.addChild("filename").setContent(convertFilename(file.getName()));
request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
if (mime != null) {
request.addChild("content-type").setContent(mime);
@ -323,6 +326,23 @@ 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());

View file

@ -79,6 +79,9 @@ public class MessageGenerator extends AbstractGenerator {
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;
}
@ -109,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;
@ -139,6 +144,8 @@ public class MessageGenerator extends AbstractGenerator {
} 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;
}

View file

@ -24,6 +24,7 @@ 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 {
@ -64,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());
@ -77,18 +78,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
new StrictHostnameVerifier());
}
try {
final SSLContext sc = SSLSocketHelper.getSSLContext();
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) {
@ -96,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));
}
}

View file

@ -25,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 {
@ -119,7 +120,7 @@ public class HttpDownloadConnection implements Transferable {
} else {
message.setTransferable(null);
}
mXmppConnectionService.updateConversationUi();
mHttpConnectionManager.updateConversationUi(true);
}
private void finish() {
@ -130,7 +131,7 @@ public class HttpDownloadConnection implements Transferable {
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
}
mXmppConnectionService.updateConversationUi();
mHttpConnectionManager.updateConversationUi(true);
if (notify) {
mXmppConnectionService.getNotificationService().push(message);
}
@ -138,11 +139,7 @@ public class HttpDownloadConnection implements Transferable {
private void changeStatus(int status) {
this.mStatus = status;
mXmppConnectionService.updateConversationUi();
}
private class WriteException extends IOException {
mHttpConnectionManager.updateConversationUi(true);
}
private void showToastForException(Exception e) {
@ -150,7 +147,7 @@ public class HttpDownloadConnection implements Transferable {
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 WriteException) {
} 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);
@ -276,16 +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) {
@ -293,9 +292,14 @@ 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;
@ -305,7 +309,7 @@ public class HttpDownloadConnection implements Transferable {
try {
os.write(buffer, 0, count);
} catch (IOException e) {
throw new WriteException();
throw new FileWriterException();
}
updateProgress((int) ((((double) transmitted) / expected) * 100));
if (canceled) {
@ -315,7 +319,7 @@ public class HttpDownloadConnection implements Transferable {
try {
os.flush();
} catch (IOException e) {
throw new WriteException();
throw new FileWriterException();
}
} catch (CancellationException | IOException e) {
throw e;
@ -336,7 +340,7 @@ public class HttpDownloadConnection implements Transferable {
public void updateProgress(int i) {
this.mProgress = i;
mXmppConnectionService.updateConversationUi();
mHttpConnectionManager.updateConversationUi(false);
}
@Override

View file

@ -182,7 +182,7 @@ public class HttpUploadConnection implements Transferable {
while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
transmitted += count;
os.write(buffer, 0, count);
mXmppConnectionService.updateConversationUi();
mHttpConnectionManager.updateConversationUi(false);
}
os.flush();
os.close();

View file

@ -74,19 +74,24 @@ public abstract class AbstractParser {
}
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");
Jid fullJid;
try {
fullJid = nick != null ? Jid.fromParts(local, domain, nick) : null;
} catch (InvalidJidException e) {
fullJid = null;
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(), nick == null ? null : fullJid);
MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid);
user.setRealJid(realJid);
user.setAffiliation(affiliation);
user.setRole(role);

View file

@ -26,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;
@ -319,6 +320,14 @@ 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);
@ -349,7 +358,8 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
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")) {

View file

@ -31,6 +31,7 @@ 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;
@ -166,11 +167,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
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) {
@ -179,7 +182,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
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;
@ -188,18 +191,22 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
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;
@ -208,7 +215,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
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");
}
@ -253,9 +260,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
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();
@ -362,7 +370,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
counterpart = from;
}
Invite invite = extractInvite(packet);
Invite invite = extractInvite(account, packet);
if (invite != null && invite.execute(account)) {
return;
}
@ -430,7 +438,18 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
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);
@ -472,7 +491,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|| replacedMessage.getFingerprint().equals(message.getFingerprint());
final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
&& replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode)) {
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();
@ -501,6 +521,12 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
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;
@ -550,9 +576,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
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);
@ -600,10 +624,19 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
+user.getRealJid()+" to "+user.getAffiliation()+" in "
+conversation.getJid().toBareJid());
if (!user.realJidMatchesAccount()) {
conversation.getMucOptions().addUser(user);
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);
}
}
}
}
}
@ -638,9 +671,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
}
}
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");

View file

@ -38,7 +38,7 @@ 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)) {
mXmppConnectionService.getAvatarService().clear(mucOptions);
@ -51,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");
@ -63,10 +64,7 @@ public class PresenceParser extends AbstractParser implements
Element item = x.findChild("item");
if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.Error.NONE);
MucOptions.User user = new MucOptions.User(mucOptions, from);
user.setAffiliation(item.getAttribute("affiliation"));
user.setRole(item.getAttribute("role"));
user.setRealJid(item.getAttributeAsJid("jid"));
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);
@ -77,7 +75,7 @@ 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()
@ -131,6 +129,10 @@ public class PresenceParser extends AbstractParser implements
Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
}
} else if (!from.isBareJid()){
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);

View file

@ -7,6 +7,7 @@ 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;
@ -21,23 +22,26 @@ 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;
@ -45,7 +49,7 @@ 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.generator.AbstractGenerator;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@ -54,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 = 29;
private static final int DATABASE_VERSION = 34;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -129,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, "
@ -139,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);
}
@ -186,7 +198,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ 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);
@ -194,6 +206,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
db.execSQL(CREATE_START_TIMES_TABLE);
}
@Override
@ -292,7 +305,16 @@ public class DatabaseBackend extends SQLiteOpenHelper {
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");
}
@ -338,6 +360,85 @@ public class DatabaseBackend extends SQLiteOpenHelper {
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) {
@ -712,6 +813,13 @@ 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 {
@ -734,6 +842,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
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};
@ -998,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());
@ -1050,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);
}
@ -1076,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());
@ -1099,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);
@ -1181,12 +1321,12 @@ 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().toPreppedString(), 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));
}
@ -1222,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);
}
}
}

View file

@ -9,6 +9,7 @@ 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;
@ -54,12 +55,14 @@ 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 static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
public static final String CONVERSATIONS_FILE_PROVIDER = "eu.siacs.conversations.files";
private static final String FILE_PROVIDER = ".files";
private XmppConnectionService mXmppConnectionService;
@ -68,7 +71,7 @@ public class FileBackend {
}
private void createNoMedia() {
final File nomedia = new File(getConversationsFileDirectory()+".nomedia");
final File nomedia = new File(getConversationsDirectory("Files")+".nomedia");
if (!nomedia.exists()) {
try {
nomedia.createNewFile();
@ -79,7 +82,8 @@ public class FileBackend {
}
public void updateMediaScanner(File file) {
if (file.getAbsolutePath().startsWith(getConversationsImageDirectory())) {
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);
@ -115,14 +119,16 @@ 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;
}
@ -151,14 +157,16 @@ public class FileBackend {
return true;
}
public static String getConversationsFileDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
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 getConversationsImageDirectory() {
return Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).getAbsolutePath()
+ "/Conversations/";
public static String getConversationsLogsDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
}
public Bitmap resize(Bitmap originalBitmap, int size) {
@ -238,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);
@ -253,7 +271,7 @@ public class FileBackend {
}
public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
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) {
@ -287,8 +305,13 @@ public class FileBackend {
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);
@ -315,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) {
@ -330,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);
@ -386,7 +406,8 @@ public class FileBackend {
return thumbnail;
}
DownloadableFile file = getFile(message);
if (file.getMimeType().startsWith("video/")) {
final String mime = file.getMimeType();
if (mime.startsWith("video/")) {
thumbnail = getVideoPreview(file, size);
} else {
Bitmap fullsize = getFullsizeImagePreview(file, size);
@ -395,6 +416,12 @@ public class FileBackend {
}
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);
}
@ -413,6 +440,21 @@ public class FileBackend {
}
}
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;
@ -425,11 +467,7 @@ public class FileBackend {
frame = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888);
frame.eraseColor(0xff000000);
}
Canvas canvas = new Canvas(frame);
Bitmap play = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), R.drawable.play_video);
float x = (frame.getWidth() - play.getWidth()) / 2.0f;
float y = (frame.getHeight() - play.getHeight()) / 2.0f;
canvas.drawBitmap(play,x,y,null);
drawOverlay(frame,R.drawable.play_video,0.75f);
return frame;
}
@ -438,17 +476,35 @@ public class FileBackend {
}
public Uri getTakePhotoUri() {
File file = new File(getTakePhotoPath()+"IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
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();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return FileProvider.getUriForFile(mXmppConnectionService, CONVERSATIONS_FILE_PROVIDER, file);
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 ("file".equals(original.getScheme())) {
if (Config.ONLY_INTERNAL_STORAGE || "file".equals(original.getScheme())) {
return original;
} else {
List<String> segments = original.getPathSegments();
@ -492,6 +548,7 @@ public class FileBackend {
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);
@ -511,6 +568,7 @@ public class FileBackend {
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;
@ -530,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");
@ -541,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());
@ -552,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;
}
@ -628,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;
@ -693,11 +753,6 @@ 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);
}

View file

@ -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);

View file

@ -44,6 +44,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
}
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) {
@ -169,7 +172,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
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);
@ -318,16 +321,18 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
letter = letter.toUpperCase(Locale.getDefault());
Paint tilePaint = new Paint(), textPaint = new Paint();
tilePaint.setColor(tileColor);
//textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
//textPaint.setColor(FG_COLOR);
//textPaint.setTypeface(Typeface.create("sans-serif-light",Typeface.NORMAL));
//textPaint.setTextSize((float) ((right - left) * 0.8));
textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(FG_COLOR);
textPaint.setTypeface(Typeface.create("sans-serif-light",
Typeface.NORMAL));
textPaint.setTextSize((float) ((right - left) * 0.8));
Rect rect = new Rect();
canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
//textPaint.getTextBounds(letter, 0, 1, rect);
//float width = textPaint.measureText(letter);
//canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom) / 2 + rect.height() / 2, textPaint);
textPaint.getTextBounds(letter, 0, 1, rect);
float width = textPaint.measureText(letter);
canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom)
/ 2 + rect.height() / 2, textPaint);
return true;
}

View file

@ -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;
}
}
}

View file

@ -19,9 +19,6 @@ public class EventReceiver extends BroadcastReceiver {
mIntentForService.setAction("other");
}
final String action = intent.getAction();
if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION) && Config.PUSH_MODE) {
return;
}
if (action.equals("ui") || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
context.startService(mIntentForService);
}

View file

@ -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);

View file

@ -56,6 +56,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
startCatchup = lastClearDate.first;
reference = null;
}
startCatchup = Math.max(startCatchup,mXmppConnectionService.getAutomaticMessageDeletionDate());
long endCatchup = account.getXmppConnection().getLastSessionEstablished();
final Query query;
if (startCatchup == 0) {
@ -107,11 +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);
query.reference = conversation.getFirstMamReference();
this.queries.add(query);
this.execute(query);
return query;
@ -220,11 +225,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
query.getConversation().setFirstMamReference(first == null ? null : first.getContent());
}
if (complete || relevant == null || abort) {
final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() == 0;
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;

View file

@ -34,6 +34,7 @@ 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;
@ -99,13 +100,28 @@ public class NotificationService {
}
}
public void finishBacklog(boolean notify) {
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)) {
@ -506,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
@ -591,7 +607,7 @@ public class NotificationService {
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);

View file

@ -20,6 +20,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
@ -37,11 +38,15 @@ import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
import net.ypresto.androidtranscoder.MediaTranscoder;
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets;
import org.openintents.openpgp.IOpenPgpService2;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;
import java.io.FileDescriptor;
import java.io.FileNotFoundException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
@ -55,9 +60,12 @@ import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.Config;
@ -65,6 +73,7 @@ import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpDecryptionService;
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.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
@ -92,16 +101,20 @@ import eu.siacs.conversations.parser.MessageParser;
import eu.siacs.conversations.parser.PresenceParser;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.ui.UiInformableCallback;
import eu.siacs.conversations.utils.ConversationsFileObserver;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
import eu.siacs.conversations.utils.PRNGFixes;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnBindListener;
import eu.siacs.conversations.xmpp.OnContactStatusChanged;
@ -260,6 +273,7 @@ public class XmppConnectionService extends Service {
private int mucRosterChangedListenerCount = 0;
private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
private int keyStatusUpdatedListenerCount = 0;
private AtomicLong mLastExpiryRun = new AtomicLong(0);
private SecureRandom mRandom;
private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20);
private final OnBindListener mOnBindListener = new OnBindListener() {
@ -295,6 +309,11 @@ public class XmppConnectionService extends Service {
mOnAccountUpdate.onAccountUpdate();
}
if (account.getStatus() == Account.State.ONLINE) {
synchronized (mLowPingTimeoutMode) {
if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
}
}
if (account.setShowErrorNotification(true)) {
databaseBackend.updateAccount(account);
}
@ -326,37 +345,35 @@ public class XmppConnectionService extends Service {
joinMuc(conversation);
}
account.pendingConferenceJoins.clear();
scheduleWakeUpCall(Config.PUSH_MODE ? Config.PING_MIN_INTERVAL : Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
} else if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
resetSendingToWaiting(account);
final boolean disabled = account.isOptionSet(Account.OPTION_DISABLED);
final boolean listeners = checkListeners();
final boolean pushMode = Config.PUSH_MODE
&& mPushManagementService.available(account)
&& listeners;
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode="+Boolean.toString(pushMode)+" listeners="+Boolean.toString(listeners));
if (!disabled && !pushMode) {
if (mLowPingTimeoutMode.contains(account.getJid().toBareJid())) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": went into offline state during low ping mode. reconnecting now");
reconnectAccount(account,true,false);
} else {
int timeToReconnect = mRandom.nextInt(20) + 10;
scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
} else {
if (account.getStatus() == Account.State.OFFLINE || account.getStatus() == Account.State.DISABLED) {
resetSendingToWaiting(account);
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
synchronized (mLowPingTimeoutMode) {
if (mLowPingTimeoutMode.contains(account.getJid().toBareJid())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": went into offline state during low ping mode. reconnecting now");
reconnectAccount(account, true, false);
} else {
int timeToReconnect = mRandom.nextInt(10) + 2;
scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode());
}
}
}
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
databaseBackend.updateAccount(account);
reconnectAccount(account, true, false);
} else if ((account.getStatus() != Account.State.CONNECTING)
&& (account.getStatus() != Account.State.NO_INTERNET)) {
resetSendingToWaiting(account);
if (connection != null) {
int next = connection.getTimeToNextAttempt();
Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ ": error connecting account. try again in "
+ next + "s for the "
+ (connection.getAttempt() + 1) + " time");
scheduleWakeUpCall(next, account.getUuid().hashCode());
}
}
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
databaseBackend.updateAccount(account);
reconnectAccount(account, true, false);
} else if ((account.getStatus() != Account.State.CONNECTING)
&& (account.getStatus() != Account.State.NO_INTERNET)) {
resetSendingToWaiting(account);
if (connection != null) {
int next = connection.getTimeToNextAttempt();
Log.d(Config.LOGTAG, account.getJid().toBareJid()
+ ": error connecting account. try again in "
+ next + "s for the "
+ (connection.getAttempt() + 1) + " time");
scheduleWakeUpCall(next, account.getUuid().hashCode());
}
}
getNotificationService().updateErrorNotification();
@ -447,10 +464,10 @@ public class XmppConnectionService extends Service {
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_FILE);
final String path = getFileBackend().getOriginalPath(uri);
mFileAddingExecutor.execute(new Runnable() {
@Override
public void run() {
private void processAsFile() {
final String path = getFileBackend().getOriginalPath(uri);
if (path != null) {
message.setRelativeFilePath(path);
getFileBackend().updateFileParams(message);
@ -478,6 +495,72 @@ public class XmppConnectionService extends Service {
}
}
}
private void processAsVideo() throws FileNotFoundException {
Log.d(Config.LOGTAG,"processing file as video");
message.setRelativeFilePath(message.getUuid() + ".mp4");
final DownloadableFile file = getFileBackend().getFile(message);
file.getParentFile().mkdirs();
ParcelFileDescriptor parcelFileDescriptor = getContentResolver().openFileDescriptor(uri, "r");
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
final ArrayList<Integer> progressTracker = new ArrayList<>();
final UiInformableCallback<Message> informableCallback;
if (callback instanceof UiInformableCallback) {
informableCallback = (UiInformableCallback<Message>) callback;
} else {
informableCallback = null;
}
MediaTranscoder.Listener listener = new MediaTranscoder.Listener() {
@Override
public void onTranscodeProgress(double progress) {
int p = ((int) Math.round(progress * 100) / 20) * 20;
if (!progressTracker.contains(p) && p != 100 && p != 0) {
progressTracker.add(p);
if (informableCallback != null) {
informableCallback.inform(getString(R.string.transcoding_video_progress, p));
}
}
}
@Override
public void onTranscodeCompleted() {
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
}
}
@Override
public void onTranscodeCanceled() {
processAsFile();
}
@Override
public void onTranscodeFailed(Exception e) {
Log.d(Config.LOGTAG,"video transcoding failed "+e.getMessage());
processAsFile();
}
};
MediaTranscoder.getInstance().transcodeVideo(fileDescriptor, file.getAbsolutePath(),
MediaFormatStrategyPresets.createAndroid720pStrategy(), listener);
}
@Override
public void run() {
final String mimeType = MimeUtils.guessMimeTypeFromUri(XmppConnectionService.this, uri);
if (mimeType != null && mimeType.startsWith("video/") && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
try {
processAsVideo();
} catch (Throwable e) {
processAsFile();
}
} else {
processAsFile();
}
}
});
}
@ -487,9 +570,13 @@ public class XmppConnectionService extends Service {
callback.error(R.string.security_error_invalid_file_access, null);
return;
}
final String mimeType = MimeUtils.guessMimeTypeFromUri(this, uri);
final String compressPictures = getCompressPicturesPreference();
if ("never".equals(compressPictures)
|| ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))) {
|| ("auto".equals(compressPictures) && getFileBackend().useImageAsIs(uri))
|| (mimeType != null && mimeType.endsWith("/gif"))) {
Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+ ": not compressing picture. sending as file");
attachFileToConversation(conversation, uri, callback);
return;
@ -543,7 +630,7 @@ public class XmppConnectionService extends Service {
switch (action) {
case ConnectivityManager.CONNECTIVITY_ACTION:
if (hasInternetConnection() && Config.RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE) {
resetAllAttemptCounts(true);
resetAllAttemptCounts(true, false);
}
break;
case ACTION_MERGE_PHONE_CONTACTS:
@ -562,14 +649,14 @@ public class XmppConnectionService extends Service {
}
break;
case ACTION_DISABLE_FOREGROUND:
getPreferences().edit().putBoolean("keep_foreground_service", false).commit();
getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false).commit();
toggleForegroundService();
break;
case ACTION_DISMISS_ERROR_NOTIFICATIONS:
dismissErrorNotifications();
break;
case ACTION_TRY_AGAIN:
resetAllAttemptCounts(false);
resetAllAttemptCounts(false, true);
interactive = true;
break;
case ACTION_REPLY_TO_CONVERSATION:
@ -597,8 +684,7 @@ public class XmppConnectionService extends Service {
refreshAllGcmTokens();
break;
case ACTION_IDLE_PING:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& !Config.PUSH_MODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scheduleNextIdlePing();
}
break;
@ -608,29 +694,58 @@ public class XmppConnectionService extends Service {
break;
}
}
this.wakeLock.acquire();
synchronized (this) {
this.wakeLock.acquire();
boolean pingNow = ConnectivityManager.CONNECTIVITY_ACTION.equals(action);
HashSet<Account> pingCandidates = new HashSet<>();
for (Account account : accounts) {
pingNow |= processAccountState(account,
interactive,
"ui".equals(action),
CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash),
pingCandidates);
}
if (pingNow) {
for (Account account : pingCandidates) {
final boolean lowTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid());
account.getXmppConnection().sendPing();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",lowTimeout=" + Boolean.toString(lowTimeout) + ")");
scheduleWakeUpCall(lowTimeout ? Config.LOW_PING_TIMEOUT : Config.PING_TIMEOUT, account.getUuid().hashCode());
}
}
if (wakeLock.isHeld()) {
try {
wakeLock.release();
} catch (final RuntimeException ignored) {
}
}
}
if (SystemClock.elapsedRealtime() - mLastExpiryRun.get() >= Config.EXPIRY_INTERVAL) {
expireOldMessages();
}
return START_STICKY;
}
private boolean processAccountState(Account account, boolean interactive, boolean isUiAction, boolean isAccountPushed, HashSet<Account> pingCandidates) {
boolean pingNow = false;
HashSet<Account> pingCandidates = new HashSet<>();
for (Account account : accounts) {
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
if (!hasInternetConnection()) {
account.setStatus(Account.State.NO_INTERNET);
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
if (!hasInternetConnection()) {
account.setStatus(Account.State.NO_INTERNET);
if (statusListener != null) {
statusListener.onStatusChanged(account);
}
} else {
if (account.getStatus() == Account.State.NO_INTERNET) {
account.setStatus(Account.State.OFFLINE);
if (statusListener != null) {
statusListener.onStatusChanged(account);
}
} else {
if (account.getStatus() == Account.State.NO_INTERNET) {
account.setStatus(Account.State.OFFLINE);
if (statusListener != null) {
statusListener.onStatusChanged(account);
}
}
if (account.getStatus() == Account.State.ONLINE) {
}
if (account.getStatus() == Account.State.ONLINE) {
synchronized (mLowPingTimeoutMode) {
long lastReceived = account.getXmppConnection().getLastPacketReceived();
long lastSent = account.getXmppConnection().getLastPingSent();
long pingInterval = (Config.PUSH_MODE || "ui".equals(action)) ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
long pingInterval = isUiAction ? Config.PING_MIN_INTERVAL * 1000 : Config.PING_MAX_INTERVAL * 1000;
long msToNextPing = (Math.max(lastReceived, lastSent) + pingInterval) - SystemClock.elapsedRealtime();
int pingTimeout = mLowPingTimeoutMode.contains(account.getJid().toBareJid()) ? Config.LOW_PING_TIMEOUT * 1000 : Config.PING_TIMEOUT * 1000;
long pingTimeoutIn = (lastSent + pingTimeout) - SystemClock.elapsedRealtime();
@ -644,7 +759,7 @@ public class XmppConnectionService extends Service {
}
} else {
pingCandidates.add(account);
if (CryptoHelper.getAccountFingerprint(account).equals(pushedAccountHash)) {
if (isAccountPushed) {
pingNow = true;
if (mLowPingTimeoutMode.add(account.getJid().toBareJid())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": entering low ping timeout mode");
@ -654,60 +769,36 @@ public class XmppConnectionService extends Service {
} else {
this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode());
if (mLowPingTimeoutMode.remove(account.getJid().toBareJid())) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": leaving low ping timeout mode");
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": leaving low ping timeout mode");
}
}
}
} else if (account.getStatus() == Account.State.OFFLINE) {
}
} else if (account.getStatus() == Account.State.OFFLINE) {
reconnectAccount(account, true, interactive);
} else if (account.getStatus() == Account.State.CONNECTING) {
long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
if (timeout < 0) {
Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting (secondsSinceLast="+secondsSinceLastConnect+")");
account.getXmppConnection().resetAttemptCount(false);
reconnectAccount(account, true, interactive);
} else if (account.getStatus() == Account.State.CONNECTING) {
long secondsSinceLastConnect = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000;
long secondsSinceLastDisco = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastDiscoStarted()) / 1000;
long discoTimeout = Config.CONNECT_DISCO_TIMEOUT - secondsSinceLastDisco;
long timeout = Config.CONNECT_TIMEOUT - secondsSinceLastConnect;
if (timeout < 0) {
Log.d(Config.LOGTAG, account.getJid() + ": time out during connect reconnecting");
account.getXmppConnection().resetAttemptCount();
reconnectAccount(account, true, interactive);
} else if (discoTimeout < 0) {
account.getXmppConnection().sendDiscoTimeout();
scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
} else {
scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
}
} else if (discoTimeout < 0) {
account.getXmppConnection().sendDiscoTimeout();
scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
} else {
if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
reconnectAccount(account, true, interactive);
}
scheduleWakeUpCall((int) Math.min(timeout,discoTimeout), account.getUuid().hashCode());
}
} else {
if (account.getXmppConnection().getTimeToNextAttempt() <= 0) {
reconnectAccount(account, true, interactive);
}
}
if (mOnAccountUpdate != null) {
mOnAccountUpdate.onAccountUpdate();
}
}
}
if (pingNow) {
final boolean listeners = checkListeners();
for (Account account : pingCandidates) {
if (listeners
&& Config.PUSH_MODE
&& mPushManagementService.available(account)) {
account.getXmppConnection().waitForPush();
cancelWakeUpCall(account.getUuid().hashCode());
} else {
account.getXmppConnection().sendPing();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + " send ping (action=" + action + ",listeners="+Boolean.toString(listeners)+")");
scheduleWakeUpCall(Config.PING_TIMEOUT, account.getUuid().hashCode());
}
}
}
if (wakeLock.isHeld()) {
try {
wakeLock.release();
} catch (final RuntimeException ignored) {
}
}
return START_STICKY;
return pingNow;
}
public boolean isDataSaverDisabled() {
@ -761,15 +852,15 @@ public class XmppConnectionService extends Service {
}
private boolean manuallyChangePresence() {
return getPreferences().getBoolean("manually_change_presence", false);
return getPreferences().getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, false);
}
private boolean treatVibrateAsSilent() {
return getPreferences().getBoolean("treat_vibrate_as_silent", false);
return getPreferences().getBoolean(SettingsActivity.TREAT_VIBRATE_AS_SILENT, false);
}
private boolean awayWhenScreenOff() {
return getPreferences().getBoolean("away_when_screen_off", false);
return getPreferences().getBoolean(SettingsActivity.AWAY_WHEN_SCREEN_IS_OFF, false);
}
private String getCompressPicturesPreference() {
@ -814,13 +905,13 @@ public class XmppConnectionService extends Service {
}
}
private void resetAllAttemptCounts(boolean reallyAll) {
private void resetAllAttemptCounts(boolean reallyAll, boolean retryImmediately) {
Log.d(Config.LOGTAG, "resetting all attempt counts");
for (Account account : accounts) {
if (account.hasErrorStatus() || reallyAll) {
final XmppConnection connection = account.getXmppConnection();
if (connection != null) {
connection.resetAttemptCount();
connection.resetAttemptCount(retryImmediately);
}
}
if (account.setShowErrorNotification(true)) {
@ -841,6 +932,33 @@ public class XmppConnectionService extends Service {
}
}
private void expireOldMessages() {
expireOldMessages(false);
}
public void expireOldMessages(final boolean resetHasMessagesLeftOnServer) {
mLastExpiryRun.set(SystemClock.elapsedRealtime());
mDatabaseExecutor.execute(new Runnable() {
@Override
public void run() {
long timestamp = getAutomaticMessageDeletionDate();
if (timestamp > 0) {
databaseBackend.expireOldMessages(timestamp);
synchronized (XmppConnectionService.this.conversations) {
for (Conversation conversation : XmppConnectionService.this.conversations) {
conversation.expireOldMessages(timestamp);
if (resetHasMessagesLeftOnServer) {
conversation.messagesLoaded.set(true);
conversation.setHasMessagesLeftOnServer(true);
}
}
}
updateConversationUi();
}
}
});
}
public boolean hasInternetConnection() {
ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE);
@ -867,6 +985,14 @@ public class XmppConnectionService extends Service {
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
if (Config.FREQUENT_RESTARTS_THRESHOLD != 0
&& Config.FREQUENT_RESTARTS_DETECTION_WINDOW != 0
&& !keepForegroundService()
&& databaseBackend.startTimeCountExceedsThreshold()) {
getPreferences().edit().putBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,true).commit();
Log.d(Config.LOGTAG,"number of restarts exceeds threshold. enabling foreground service");
}
restoreFromDatabase();
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
@ -897,10 +1023,11 @@ public class XmppConnectionService extends Service {
this.pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
this.wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "XmppConnectionService");
toggleForegroundService();
updateUnreadCountBadge();
toggleScreenEventReceiver();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !Config.PUSH_MODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
scheduleNextIdlePing();
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@ -943,17 +1070,21 @@ public class XmppConnectionService extends Service {
}
public void toggleForegroundService() {
if (getPreferences().getBoolean("keep_foreground_service", false)) {
if (keepForegroundService()) {
startForeground(NotificationService.FOREGROUND_NOTIFICATION_ID, this.mNotificationService.createForegroundNotification());
} else {
stopForeground(true);
}
}
private boolean keepForegroundService() {
return getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE,false);
}
@Override
public void onTaskRemoved(final Intent rootIntent) {
super.onTaskRemoved(rootIntent);
if (!getPreferences().getBoolean("keep_foreground_service", false)) {
if (!keepForegroundService()) {
this.logoutAndSave(false);
} else {
Log.d(Config.LOGTAG,"ignoring onTaskRemoved because foreground service is activated");
@ -962,6 +1093,7 @@ public class XmppConnectionService extends Service {
private void logoutAndSave(boolean stop) {
int activeAccounts = 0;
databaseBackend.clearStartTimeCounter(true); // regular swipes don't count towards restart counter
for (final Account account : accounts) {
if (account.getStatus() != Account.State.DISABLED) {
activeAccounts++;
@ -982,13 +1114,6 @@ public class XmppConnectionService extends Service {
}
}
private void cancelWakeUpCall(int requestCode) {
final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
final Intent intent = new Intent(this, EventReceiver.class);
intent.setAction("ping");
alarmManager.cancel(PendingIntent.getBroadcast(this, requestCode, intent, 0));
}
public void scheduleWakeUpCall(int seconds, int requestCode) {
final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000;
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
@ -1212,12 +1337,10 @@ public class XmppConnectionService extends Service {
if (addToConversation) {
conversation.add(message);
}
if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) {
if (saveInDb) {
databaseBackend.createMessage(message);
} else if (message.edited()) {
databaseBackend.updateMessage(message, message.getEditedId());
}
if (saveInDb) {
databaseBackend.createMessage(message);
} else if (message.edited()) {
databaseBackend.updateMessage(message, message.getEditedId());
}
updateConversationUi();
}
@ -1327,6 +1450,12 @@ public class XmppConnectionService extends Service {
Runnable runnable = new Runnable() {
@Override
public void run() {
long deletionDate = getAutomaticMessageDeletionDate();
mLastExpiryRun.set(SystemClock.elapsedRealtime());
if (deletionDate > 0) {
Log.d(Config.LOGTAG, "deleting messages that are older than "+AbstractGenerator.getTimestamp(deletionDate));
databaseBackend.expireOldMessages(deletionDate);
}
Log.d(Config.LOGTAG, "restoring roster");
for (Account account : accounts) {
databaseBackend.readRoster(account.getRoster());
@ -1498,8 +1627,11 @@ public class XmppConnectionService extends Service {
MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp);
if (query != null) {
query.setCallback(callback);
callback.informUser(R.string.fetching_history_from_server);
} else {
callback.informUser(R.string.not_fetching_history_retention_period);
}
callback.informUser(R.string.fetching_history_from_server);
}
}
}
@ -1655,7 +1787,7 @@ public class XmppConnectionService extends Service {
callback.onAccountCreated(account);
if (Config.X509_VERIFICATION) {
try {
getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
} catch (CertificateException e) {
callback.informUser(R.string.certificate_chain_is_not_trusted);
}
@ -1683,7 +1815,7 @@ public class XmppConnectionService extends Service {
databaseBackend.updateAccount(account);
if (Config.X509_VERIFICATION) {
try {
getMemorizingTrustManager().getNonInteractive().checkClientTrusted(chain, "RSA");
getMemorizingTrustManager().getNonInteractive(account.getJid().getDomainpart()).checkClientTrusted(chain, "RSA");
} catch (CertificateException e) {
showErrorToastInUi(R.string.certificate_chain_is_not_trusted);
}
@ -2011,10 +2143,6 @@ public class XmppConnectionService extends Service {
if (connection.getFeatures().csi()) {
connection.sendInactive();
}
if (Config.PUSH_MODE && mPushManagementService.available(account)) {
connection.waitForPush();
cancelWakeUpCall(account.getUuid().hashCode());
}
}
}
}
@ -2032,10 +2160,18 @@ public class XmppConnectionService extends Service {
}
public void joinMuc(Conversation conversation) {
joinMuc(conversation, null);
joinMuc(conversation,null, false);
}
public void joinMuc(Conversation conversation, boolean followedInvite) {
joinMuc(conversation, null, followedInvite);
}
private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) {
joinMuc(conversation,onConferenceJoined,false);
}
private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined, final boolean followedInvite) {
Account account = conversation.getAccount();
account.pendingConferenceJoins.remove(conversation);
account.pendingConferenceLeaves.remove(conversation);
@ -2080,6 +2216,9 @@ public class XmppConnectionService extends Service {
}
if (mucOptions.membersOnly() && mucOptions.nonanonymous()) {
fetchConferenceMembers(conversation);
if (followedInvite && conversation.getBookmark() == null) {
saveConversationAsBookmark(conversation,null);
}
}
sendUnsentMessages(conversation);
}
@ -2114,6 +2253,7 @@ public class XmppConnectionService extends Service {
OnIqPacketReceived callback = new OnIqPacketReceived() {
private int i = 0;
private boolean success = true;
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
@ -2124,19 +2264,37 @@ public class XmppConnectionService extends Service {
if ("item".equals(child.getName())) {
MucOptions.User user = AbstractParser.parseItem(conversation,child);
if (!user.realJidMatchesAccount()) {
conversation.getMucOptions().addUser(user);
getAvatarService().clear(conversation);
updateMucRosterUi();
updateConversationUi();
conversation.getMucOptions().updateUser(user);
}
}
}
} else {
success = false;
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not request affiliation "+affiliations[i]+" in "+conversation.getJid().toBareJid());
}
++i;
if (i >= affiliations.length) {
List<Jid> members = conversation.getMucOptions().getMembers();
if (success) {
List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
boolean changed = false;
for(ListIterator<Jid> iterator = cryptoTargets.listIterator(); iterator.hasNext();) {
Jid jid = iterator.next();
if (!members.contains(jid)) {
iterator.remove();
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": removed "+jid+" from crypto targets of "+conversation.getName());
changed = true;
}
}
if (changed) {
conversation.setAcceptedCryptoTargets(cryptoTargets);
updateConversation(conversation);
}
}
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved members for "+conversation.getJid().toBareJid()+": "+conversation.getMucOptions().getMembers());
getAvatarService().clear(conversation);
updateMucRosterUi();
updateConversationUi();
}
}
};
@ -2613,14 +2771,13 @@ public class XmppConnectionService extends Service {
}
public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) {
final IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
IqPacket packet = this.mIqGenerator.publishAvatar(avatar);
this.sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket result) {
if (result.getType() == IqPacket.TYPE.RESULT) {
final IqPacket packet = XmppConnectionService.this.mIqGenerator
.publishAvatarMetadata(avatar);
final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar);
sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket result) {
@ -2629,25 +2786,22 @@ public class XmppConnectionService extends Service {
getAvatarService().clear(account);
databaseBackend.updateAccount(account);
}
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar "+(avatar.size/1024)+"KiB");
if (callback != null) {
callback.success(avatar);
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar");
}
} else {
if (callback != null) {
callback.error(
R.string.error_publish_avatar_server_reject,
avatar);
callback.error(R.string.error_publish_avatar_server_reject,avatar);
}
}
}
});
} else {
Element error = result.findChild("error");
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server rejected avatar "+(avatar.size/1024)+"KiB "+(error!=null?error.toString():""));
if (callback != null) {
callback.error(
R.string.error_publish_avatar_server_reject,
avatar);
callback.error(R.string.error_publish_avatar_server_reject, avatar);
}
}
}
@ -2890,23 +3044,26 @@ public class XmppConnectionService extends Service {
if (connection == null) {
connection = createConnection(account);
account.setXmppConnection(connection);
} else {
connection.interrupt();
}
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
boolean hasInternet = hasInternetConnection();
if (!account.isOptionSet(Account.OPTION_DISABLED) && hasInternet) {
if (!force) {
disconnect(account, false);
}
Thread thread = new Thread(connection);
connection.setInteractive(interactive);
connection.prepareNewConnection();
connection.interrupt();
thread.start();
scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
} else {
disconnect(account, force);
disconnect(account, force || account.getTrueStatus().isError() || !hasInternet);
account.getRoster().clearPresences();
connection.resetEverything();
account.getAxolotlService().resetBrokenness();
if (!hasInternet) {
account.setStatus(Account.State.NO_INTERNET);
}
}
}
}
@ -2987,8 +3144,7 @@ public class XmppConnectionService extends Service {
public void markMessage(Message message, int status, String errorMessage) {
if (status == Message.STATUS_SEND_FAILED
&& (message.getStatus() == Message.STATUS_SEND_RECEIVED || message
.getStatus() == Message.STATUS_SEND_DISPLAYED)
&& errorMessage == null) {
.getStatus() == Message.STATUS_SEND_DISPLAYED)) {
return;
}
message.setErrorMessage(errorMessage);
@ -3002,6 +3158,15 @@ public class XmppConnectionService extends Service {
.getDefaultSharedPreferences(getApplicationContext());
}
public long getAutomaticMessageDeletionDate() {
try {
final long timeout = Long.parseLong(getPreferences().getString(SettingsActivity.AUTOMATIC_MESSAGE_DELETION, "0")) * 1000;
return timeout == 0 ? timeout : System.currentTimeMillis() - timeout;
} catch (NumberFormatException e) {
return 0;
}
}
public boolean confirmMessages() {
return getPreferences().getBoolean("confirm_messages", true);
}
@ -3014,10 +3179,6 @@ public class XmppConnectionService extends Service {
return getPreferences().getBoolean("chat_states", false);
}
public boolean saveEncryptedMessages() {
return !getPreferences().getBoolean("dont_save_encrypted", false);
}
private boolean respectAutojoin() {
return getPreferences().getBoolean("autojoin", true);
}
@ -3603,6 +3764,68 @@ public class XmppConnectionService extends Service {
conversation.setBookmark(bookmark);
}
public void clearStartTimeCounter() {
mDatabaseExecutor.execute(new Runnable() {
@Override
public void run() {
databaseBackend.clearStartTimeCounter(false);
}
});
}
public boolean verifyFingerprints(Contact contact, List<XmppUri.Fingerprint> fingerprints) {
boolean needsRosterWrite = false;
boolean performedVerification = false;
final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
for(XmppUri.Fingerprint fp : fingerprints) {
if (fp.type == XmppUri.FingerprintType.OTR) {
performedVerification |= contact.addOtrFingerprint(fp.fingerprint);
needsRosterWrite |= performedVerification;
} else if (fp.type == XmppUri.FingerprintType.OMEMO) {
String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
if (fingerprintStatus != null) {
if (!fingerprintStatus.isVerified()) {
performedVerification = true;
axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
}
} else {
axolotlService.preVerifyFingerprint(contact,fingerprint);
}
}
}
if (needsRosterWrite) {
syncRosterToDisk(contact.getAccount());
}
return performedVerification;
}
public boolean verifyFingerprints(Account account, List<XmppUri.Fingerprint> fingerprints) {
final AxolotlService axolotlService = account.getAxolotlService();
boolean verifiedSomething = false;
for(XmppUri.Fingerprint fp : fingerprints) {
if (fp.type == XmppUri.FingerprintType.OMEMO) {
String fingerprint = "05"+fp.fingerprint.replaceAll("\\s","");
Log.d(Config.LOGTAG,"trying to verify own fp="+fingerprint);
FingerprintStatus fingerprintStatus = axolotlService.getFingerprintTrust(fingerprint);
if (fingerprintStatus != null) {
if (!fingerprintStatus.isVerified()) {
axolotlService.setFingerprintTrust(fingerprint,fingerprintStatus.toVerified());
verifiedSomething = true;
}
} else {
axolotlService.preVerifyFingerprint(account,fingerprint);
verifiedSomething = true;
}
}
}
return verifiedSomething;
}
public boolean blindTrustBeforeVerification() {
return getPreferences().getBoolean(SettingsActivity.BLIND_TRUST_BEFORE_VERIFICATION, true);
}
public interface OnMamPreferencesFetched {
void onPreferencesFetched(Element prefs);
void onPreferencesFetchFailed();

View file

@ -37,7 +37,6 @@ public final class BlockContactDialog {
builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain);
value = blockable.getJid().toDomainJid().toString();
spannable = new SpannableString(context.getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text, value));
message.setText(spannable);
} else {
builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact);
value = blockable.getJid().toBareJid().toString();

View file

@ -2,10 +2,8 @@ package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.view.ContextMenu;
@ -44,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() {
@ -380,7 +381,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
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);
@ -511,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();
}
@ -520,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;
@ -621,6 +622,12 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
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);

View file

@ -32,29 +32,29 @@ 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;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.entities.Presence;
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;
@ -113,11 +113,14 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
private CheckBox send;
private CheckBox receive;
private Button addContactButton;
private Button mShowInactiveDevicesButton;
private QuickContactBadge badge;
private LinearLayout keys;
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() {
@ -189,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));
@ -217,11 +221,26 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
});
keys = (LinearLayout) findViewById(R.id.details_contact_keys);
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();
}
});
}
@Override
public void onSaveInstanceState(final Bundle savedInstanceState) {
savedInstanceState.putBoolean("show_inactive_omemo",showInactiveOmemo);
super.onSaveInstanceState(savedInstanceState);
}
@Override
@ -445,15 +464,32 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
}
if (Config.supportOmemo()) {
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);
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 (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
hasKeys = true;
@ -487,11 +523,7 @@ 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(this);
if (tagList.size() == 0 || !this.showDynamicTags) {
@ -508,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 (Config.X509_VERIFICATION && 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);
@ -562,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();
}
}
@ -579,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();
}
}
}

View file

@ -49,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;
@ -64,6 +65,7 @@ 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;
@ -97,6 +99,7 @@ public class ConversationActivity extends XmppActivity
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<>();
@ -119,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;
@ -131,6 +135,7 @@ public class ConversationActivity extends XmppActivity
public void showConversationsOverview() {
if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
mShouldPanelBeOpen.set(true);
mSlidingPaneLayout.openPane();
}
}
@ -148,6 +153,7 @@ public class ConversationActivity extends XmppActivity
public void hideConversationsOverview() {
if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
mShouldPanelBeOpen.set(false);
mSlidingPaneLayout.closePane();
}
}
@ -158,8 +164,7 @@ public class ConversationActivity extends XmppActivity
public boolean isConversationsOverviewVisable() {
if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
return mSlidingPaneLayout.isOpen();
return mShouldPanelBeOpen.get();
} else {
return true;
}
@ -293,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();
}
@ -374,7 +378,7 @@ public class ConversationActivity extends XmppActivity
}
public void sendReadMarkerIfNecessary(final Conversation conversation) {
if (!mActivityPaused && conversation != null) {
if (!mActivityPaused && !mUnprocessedNewIntent && conversation != null) {
xmppConnectionService.sendReadMarker(conversation);
}
}
@ -494,6 +498,7 @@ public class ConversationActivity extends XmppActivity
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();
@ -548,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;
}
}
@ -645,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;
}
@ -967,7 +972,7 @@ public class ConversationActivity extends XmppActivity
if (!isConversationsOverviewVisable()) {
showConversationsOverview();
} else {
moveTaskToBack(true);
super.onBackPressed();
}
}
@ -989,6 +994,7 @@ 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;
@ -1086,6 +1092,7 @@ public class ConversationActivity extends XmppActivity
protected void onNewIntent(final Intent intent) {
if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
mOpenConversation = null;
mUnprocessedNewIntent = true;
if (xmppConnectionServiceBound) {
handleViewConversationIntent(intent);
intent.setAction(Intent.ACTION_MAIN);
@ -1124,6 +1131,7 @@ public class ConversationActivity extends XmppActivity
}
this.mActivityPaused = false;
if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) {
sendReadMarkerIfNecessary(getSelectedConversation());
}
@ -1262,6 +1270,11 @@ public class ConversationActivity extends XmppActivity
if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) {
openBatteryOptimizationDialogIfNeeded();
}
if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
xmppConnectionService.getNotificationService().setOpenConversation(null);
} else {
xmppConnectionService.getNotificationService().setOpenConversation(getSelectedConversation());
}
}
private void handleViewConversationIntent(final Intent intent) {
@ -1288,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
@ -1298,6 +1312,8 @@ public class ConversationActivity extends XmppActivity
startDownloadable(message);
}
}
} else {
mUnprocessedNewIntent = false;
}
}
@ -1409,9 +1425,11 @@ public class ConversationActivity extends XmppActivity
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();
}
@ -1446,7 +1464,8 @@ public class ConversationActivity extends XmppActivity
}
private long getMaxHttpUploadSize(Conversation conversation) {
return conversation.getAccount().getXmppConnection().getFeatures().getMaxHttpUploadSize();
final XmppConnection connection = conversation.getAccount().getXmppConnection();
return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
}
private void setNeverAskForBatteryOptimizationsAgain() {
@ -1488,7 +1507,7 @@ public class ConversationActivity extends XmppActivity
private boolean hasAccountWithoutPush() {
for(Account account : xmppConnectionService.getAccounts()) {
if (account.getStatus() != Account.State.DISABLED
&& !xmppConnectionService.getPushManagementService().available(account)) {
&& !xmppConnectionService.getPushManagementService().availableAndUseful(account)) {
return true;
}
}
@ -1524,9 +1543,26 @@ 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);
}
@ -1550,6 +1586,10 @@ public class ConversationActivity extends XmppActivity
});
}
public void attachImageToConversation(Uri uri) {
this.attachImageToConversation(getSelectedConversation(), uri);
}
private void attachImageToConversation(Conversation conversation, Uri uri) {
if (conversation == null) {
return;
@ -1669,8 +1709,8 @@ public class ConversationActivity extends XmppActivity
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, targets).isEmpty();
boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
@ -1747,11 +1787,4 @@ public class ConversationActivity extends XmppActivity
public boolean highlightSelectedConversations() {
return !isConversationsOverviewHideable() || this.conversationWasSelectedByKeyboard;
}
public void setMessagesLoaded() {
if (mConversationFragment != null) {
mConversationFragment.setMessagesLoaded();
mConversationFragment.updateMessages();
}
}
}

View file

@ -9,8 +9,13 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.text.Editable;
import android.text.InputType;
import android.util.Log;
import android.util.Pair;
@ -29,6 +34,7 @@ import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.RelativeLayout;
@ -55,6 +61,7 @@ import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.http.HttpDownloadConnection;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
@ -113,7 +120,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private RelativeLayout snackbar;
private TextView snackbarMessage;
private TextView snackbarAction;
private boolean messagesLoaded = true;
private Toast messageLoaderToast;
private OnScrollListener mOnScrollListener = new OnScrollListener() {
@ -128,14 +134,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
public void onScroll(AbsListView view, int firstVisibleItem,
int visibleItemCount, int totalItemCount) {
synchronized (ConversationFragment.this.messageList) {
if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) {
if (firstVisibleItem < 5 && conversation != null && conversation.messagesLoaded.compareAndSet(true,false) && messageList.size() > 0) {
long timestamp;
if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) {
timestamp = messageList.get(1).getTimeSent();
} else {
timestamp = messageList.get(0).getTimeSent();
}
messagesLoaded = false;
activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() {
@Override
public void onMoreMessagesLoaded(final int c, Conversation conversation) {
@ -164,7 +169,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
messageListAdapter.notifyDataSetChanged();
int pos = Math.max(getIndexOf(uuid,messageList),0);
messagesView.setSelectionFromTop(pos, pxOffset);
messagesLoaded = true;
if (messageLoaderToast != null) {
messageLoaderToast.cancel();
}
@ -284,6 +288,37 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
}
};
private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() {
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) {
// try to get permission to read the image, if applicable
if ((flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {
Log.e(Config.LOGTAG, "InputContentInfoCompat#requestPermission() failed.", e);
Toast.makeText(
activity,
activity.getString(R.string.no_permission_to_access_x, inputContentInfo.getDescription()),
Toast.LENGTH_LONG
).show();
return false;
}
}
// send the image
activity.attachImageToConversation(inputContentInfo.getContentUri());
// TODO: revoke permissions?
// since uploading an image is async its tough to wire a callback to when
// the image has finished uploading.
// According to the docs: "calling IC#releasePermission() is just to be a
// good citizen. Even if we failed to call that method, the system would eventually revoke
// the permission sometime after inputContentInfo object gets garbage-collected."
// See: https://developer.android.com/samples/CommitContentSampleApp/src/com.example.android.commitcontent.app/MainActivity.html#l164
return true;
}
};
private OnClickListener mSendButtonListener = new OnClickListener() {
@Override
@ -338,10 +373,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private ConversationActivity activity;
private Message selectedMessage;
public void setMessagesLoaded() {
this.messagesLoaded = true;
}
private void sendMessage() {
final String body = mEditMessage.getText().toString();
if (body.length() == 0 || this.conversation == null) {
@ -415,6 +446,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
public View onCreateView(final LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_conversation, container, false);
view.setOnClickListener(null);
String[] allImagesMimeType = {"image/*"};
mEditMessage = (EditMessage) view.findViewById(R.id.textinput);
mEditMessage.setOnClickListener(new OnClickListener() {
@ -426,6 +459,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
});
mEditMessage.setOnEditorActionListener(mEditorActionListener);
mEditMessage.setRichContentListener(allImagesMimeType, mEditorContentListener);
mSendButton = (ImageButton) view.findViewById(R.id.textSendButton);
mSendButton.setOnClickListener(this.mSendButtonListener);
@ -506,6 +540,34 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
}
});
messageListAdapter.setOnQuoteListener(new MessageAdapter.OnQuoteListener() {
@Override
public void onQuote(String text) {
if (mEditMessage.isEnabled()) {
text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
Editable editable = mEditMessage.getEditableText();
int position = mEditMessage.getSelectionEnd();
if (position == -1) position = editable.length();
if (position > 0 && editable.charAt(position - 1) != '\n') {
editable.insert(position++, "\n");
}
editable.insert(position, text);
position += text.length();
editable.insert(position++, "\n");
if (position < editable.length() && editable.charAt(position) != '\n') {
editable.insert(position, "\n");
}
mEditMessage.setSelection(position);
mEditMessage.requestFocus();
InputMethodManager inputMethodManager = (InputMethodManager) getActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT);
}
}
}
});
messagesView.setAdapter(messageListAdapter);
registerForContextMenu(messagesView);
@ -557,7 +619,8 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
retryDecryption.setVisible(true);
}
if (relevantForCorrection.getType() == Message.TYPE_TEXT
&& relevantForCorrection.isLastCorrectableMessage()) {
&& relevantForCorrection.isLastCorrectableMessage()
&& (m.getConversation().getMucOptions().nonanonymous() || m.getConversation().getMode() == Conversation.MODE_SINGLE)) {
correctMessage.setVisible(true);
}
if (treatAsFile || (GeoHelper.isGeoUri(m.getBody()))) {
@ -652,9 +715,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody());
shareIntent.setType("text/plain");
} else {
shareIntent.putExtra(Intent.EXTRA_STREAM,
activity.xmppConnectionService.getFileBackend()
.getJingleFileUri(message));
final DownloadableFile file = activity.xmppConnectionService.getFileBackend().getFile(message);
try {
shareIntent.putExtra(Intent.EXTRA_STREAM, FileBackend.getUriForFile(activity, file));
} catch (SecurityException e) {
Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
return;
}
shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
String mime = message.getMimeType();
if (mime == null) {
@ -776,16 +843,22 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
protected void highlightInConference(String nick) {
String oldString = mEditMessage.getText().toString().trim();
if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) {
final Editable editable = mEditMessage.getText();
String oldString = editable.toString().trim();
final int pos = mEditMessage.getSelectionStart();
if (oldString.isEmpty() || pos == 0) {
mEditMessage.getText().insert(0, nick + ": ");
} else {
if (mEditMessage.getText().charAt(
mEditMessage.getSelectionStart() - 1) != ' ') {
nick = " " + nick;
final char before = editable.charAt(pos - 1);
final char after = editable.length() > pos ? editable.charAt(pos) : '\0';
if (before == '\n') {
editable.insert(pos, nick + ": ");
} else {
editable.insert(pos,(Character.isWhitespace(before)? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " "));
if (Character.isWhitespace(after)) {
mEditMessage.setSelection(mEditMessage.getSelectionStart()+1);
}
}
mEditMessage.getText().insert(mEditMessage.getSelectionStart(),
nick + " ");
}
}
@ -833,7 +906,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
messageListAdapter.updatePreferences();
this.messagesView.setAdapter(messageListAdapter);
updateMessages();
this.messagesLoaded = true;
this.conversation.messagesLoaded.set(true);
synchronized (this.messageList) {
final Message first = conversation.getFirstUnreadMessage();
final int bottom = Math.max(0, this.messageList.size() - 1);
@ -916,15 +989,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private void updateSnackBar(final Conversation conversation) {
final Account account = conversation.getAccount();
final Contact contact = conversation.getContact();
final int mode = conversation.getMode();
final Contact contact = mode == Conversation.MODE_SINGLE ? conversation.getContact() : null;
if (account.getStatus() == Account.State.DISABLED) {
showSnackbar(R.string.this_account_is_disabled, R.string.enable, this.mEnableAccountListener);
} else if (conversation.isBlocked()) {
showSnackbar(R.string.contact_blocked, R.string.unblock, this.mUnblockClickListener);
} else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
} else if (contact != null && !contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
showSnackbar(R.string.contact_added_you, R.string.add_back, this.mAddBackClickListener);
} else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
} else if (contact != null && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
showSnackbar(R.string.contact_asks_for_presence_subscription, R.string.allow, this.mAllowPresenceSubscription);
} else if (mode == Conversation.MODE_MULTI
&& !conversation.getMucOptions().online()

View file

@ -35,8 +35,6 @@ import android.widget.TableRow;
import android.widget.TextView;
import android.widget.Toast;
import android.util.Log;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
@ -45,15 +43,19 @@ import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.XmppConnectionService.OnCaptchaRequested;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.XmppConnection.Features;
import eu.siacs.conversations.xmpp.forms.Data;
@ -61,20 +63,23 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class EditAccountActivity extends XmppActivity implements OnAccountUpdate,
public class EditAccountActivity extends OmemoActivity implements OnAccountUpdate, OnUpdateBlocklist,
OnKeyStatusUpdated, OnCaptchaRequested, KeyChainAliasCallback, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnMamPreferencesFetched {
private static final int REQUEST_DATA_SAVER = 0x37af244;
private AutoCompleteTextView mAccountJid;
private EditText mPassword;
private EditText mPasswordConfirm;
private CheckBox mRegisterNew;
private Button mCancelButton;
private Button mSaveButton;
private Button mDisableBatterOptimizations;
private Button mDisableOsOptimizationsButton;
private TextView mDisableOsOptimizationsHeadline;
private TextView getmDisableOsOptimizationsBody;
private TableLayout mMoreTable;
private LinearLayout mStats;
private RelativeLayout mBatteryOptimizations;
private RelativeLayout mOsOptimizations;
private TextView mServerInfoSm;
private TextView mServerInfoRosterVersion;
private TextView mServerInfoCarbons;
@ -94,7 +99,6 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private RelativeLayout mAxolotlFingerprintBox;
private ImageButton mOtrFingerprintToClipboardButton;
private ImageButton mAxolotlFingerprintToClipboardButton;
private ImageButton mRegenerateAxolotlKeyButton;
private LinearLayout keys;
private LinearLayout keysCard;
private LinearLayout mNamePort;
@ -249,6 +253,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private TableRow mPushRow;
private String mSavedInstanceAccount;
private boolean mSavedInstanceInit = false;
private Button mClearDevicesButton;
public void refreshUiReal() {
invalidateOptionsMenu();
@ -371,13 +376,24 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_BATTERY_OP) {
if (requestCode == REQUEST_BATTERY_OP || requestCode == REQUEST_DATA_SAVER) {
updateAccountInformation(mAccount == null);
}
}
@Override
protected void processFingerprintVerification(XmppUri uri) {
if (mAccount != null && mAccount.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
if (xmppConnectionService.verifyFingerprints(mAccount,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();
}
}
protected void updateSaveButton() {
boolean accountInfoEdited = accountInfoEdited();
@ -472,21 +488,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mAvatar.setOnClickListener(this.mAvatarClickListener);
this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new);
this.mStats = (LinearLayout) findViewById(R.id.stats);
this.mBatteryOptimizations = (RelativeLayout) findViewById(R.id.battery_optimization);
this.mDisableBatterOptimizations = (Button) findViewById(R.id.batt_op_disable);
this.mDisableBatterOptimizations.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
Uri uri = Uri.parse("package:"+getPackageName());
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_BATTERY_OP);
} catch (ActivityNotFoundException e) {
Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
}
}
});
this.mOsOptimizations = (RelativeLayout) findViewById(R.id.os_optimization);
this.mDisableOsOptimizationsButton = (Button) findViewById(R.id.os_optimization_disable);
this.mDisableOsOptimizationsHeadline = (TextView) findViewById(R.id.os_optimization_headline);
this.getmDisableOsOptimizationsBody = (TextView) findViewById(R.id.os_optimization_body);
this.mSessionEst = (TextView) findViewById(R.id.session_est);
this.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version);
this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons);
@ -504,13 +509,19 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint);
this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box);
this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard);
this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key);
this.mOwnFingerprintDesc = (TextView) findViewById(R.id.own_fingerprint_desc);
this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card);
this.keys = (LinearLayout) findViewById(R.id.other_device_keys);
this.mNamePort = (LinearLayout) findViewById(R.id.name_port);
this.mHostname = (EditText) findViewById(R.id.hostname);
this.mHostname.addTextChangedListener(mTextWatcher);
this.mClearDevicesButton = (Button) findViewById(R.id.clear_devices);
this.mClearDevicesButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showWipePepDialog();
}
});
this.mPort = (EditText) findViewById(R.id.port);
this.mPort.setText("5222");
this.mPort.addTextChangedListener(mTextWatcher);
@ -549,31 +560,30 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
final MenuItem showPassword = menu.findItem(R.id.action_show_password);
final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
final MenuItem renewCertificate = menu.findItem(R.id.action_renew_certificate);
final MenuItem mamPrefs = menu.findItem(R.id.action_mam_prefs);
final MenuItem changePresence = menu.findItem(R.id.action_change_presence);
final MenuItem share = menu.findItem(R.id.action_share);
renewCertificate.setVisible(mAccount != null && mAccount.getPrivateKeyAlias() != null);
share.setVisible(mAccount != null && !mInitMode);
if (mAccount != null && mAccount.isOnlineAndConnected()) {
if (!mAccount.getXmppConnection().getFeatures().blocking()) {
showBlocklist.setVisible(false);
} else {
showBlocklist.setEnabled(mAccount.getBlocklist().size() > 0);
}
if (!mAccount.getXmppConnection().getFeatures().register()) {
changePassword.setVisible(false);
}
mamPrefs.setVisible(mAccount.getXmppConnection().getFeatures().mam());
Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
if (otherDevices == null || otherDevices.isEmpty() || !Config.supportOmemo()) {
clearDevices.setVisible(false);
}
changePresence.setVisible(manuallyChangePresence());
} else {
showQrCode.setVisible(false);
showBlocklist.setVisible(false);
showMoreInfo.setVisible(false);
changePassword.setVisible(false);
clearDevices.setVisible(false);
mamPrefs.setVisible(false);
changePresence.setVisible(false);
}
@ -645,7 +655,6 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
super.onSaveInstanceState(savedInstanceState);
}
@Override
protected void onBackendConnected() {
boolean init = true;
if (mSavedInstanceAccount != null) {
@ -670,6 +679,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mPassword.requestFocus();
}
}
if (mPendingFingerprintVerificationUri != null) {
processFingerprintVerification(mPendingFingerprintVerificationUri);
mPendingFingerprintVerificationUri = null;
}
updateAccountInformation(init);
}
@ -710,15 +723,21 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
case R.id.action_server_info_show_more:
changeMoreTableVisibility(!item.isChecked());
break;
case R.id.action_share_barcode:
shareBarcode();
break;
case R.id.action_share_http:
shareLink(true);
break;
case R.id.action_share_uri:
shareLink(false);
break;
case R.id.action_change_password_on_server:
gotoChangePassword(null);
break;
case R.id.action_mam_prefs:
editMamPrefs();
break;
case R.id.action_clear_devices:
showWipePepDialog();
break;
case R.id.action_renew_certificate:
renewCertificate();
break;
@ -732,6 +751,27 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
return super.onOptionsItemSelected(item);
}
private void shareLink(boolean http) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
String text;
if (http) {
text = mAccount.getShareableLink();
} else {
text = mAccount.getShareableUri();
}
intent.putExtra(Intent.EXTRA_TEXT,text);
startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
}
private void shareBarcode() {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM,BarcodeProvider.getUriForAccount(this,mAccount));
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setType("image/png");
startActivity(Intent.createChooser(intent, getText(R.string.share_with)));
}
private void changeMoreTableVisibility(boolean visible) {
mMoreTable.setVisibility(visible ? View.VISIBLE : View.GONE);
}
@ -796,8 +836,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) {
Features features = this.mAccount.getXmppConnection().getFeatures();
this.mStats.setVisibility(View.VISIBLE);
boolean showOptimizingWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery();
this.mBatteryOptimizations.setVisibility(showOptimizingWarning ? View.VISIBLE : View.GONE);
boolean showBatteryWarning = !xmppConnectionService.getPushManagementService().availableAndUseful(mAccount) && isOptimizingBattery();
boolean showDataSaverWarning = isAffectedByDataSaver();
showOsOptimizationWarning(showBatteryWarning,showDataSaverWarning);
this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection()
.getLastSessionEstablished()));
if (features.rosterVersioning()) {
@ -866,7 +907,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View v) {
if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(otrFingerprint), R.string.otr_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_otr_fingerprint,
@ -893,41 +934,29 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View v) {
if (copyTextToClipboard(ownAxolotlFingerprint.substring(2), R.string.omemo_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_omemo_fingerprint,
Toast.LENGTH_SHORT).show();
}
copyOmemoFingerprint(ownAxolotlFingerprint);
}
});
if (Config.SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON) {
this.mRegenerateAxolotlKeyButton
.setVisibility(View.VISIBLE);
this.mRegenerateAxolotlKeyButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
showRegenerateAxolotlKeyDialog();
}
});
}
} else {
this.mAxolotlFingerprintBox.setVisibility(View.GONE);
}
boolean hasKeys = false;
keys.removeAllViews();
for (final String fingerprint : mAccount.getAxolotlService().getFingerprintsForOwnSessions()) {
if (ownAxolotlFingerprint.equals(fingerprint)) {
continue;
for(XmppAxolotlSession session : mAccount.getAxolotlService().findOwnSessions()) {
if (!session.getTrust().isCompromised()) {
boolean highlight = session.getFingerprint().equals(messageFingerprint);
addFingerprintRow(keys,session,highlight);
hasKeys = true;
}
boolean highlight = fingerprint.equals(messageFingerprint);
hasKeys |= addFingerprintRow(keys, mAccount, fingerprint, highlight, null);
}
if (hasKeys && Config.supportOmemo()) {
keysCard.setVisibility(View.VISIBLE);
Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
if (otherDevices == null || otherDevices.isEmpty()) {
mClearDevicesButton.setVisibility(View.GONE);
} else {
mClearDevicesButton.setVisibility(View.VISIBLE);
}
} else {
keysCard.setVisibility(View.GONE);
}
@ -956,20 +985,43 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
}
public void showRegenerateAxolotlKeyDialog() {
Builder builder = new Builder(this);
builder.setTitle("Regenerate Key");
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)");
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton("Yes",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAccount.getAxolotlService().regenerateKeys(false);
private void showOsOptimizationWarning(boolean showBatteryWarning, boolean showDataSaverWarning) {
this.mOsOptimizations.setVisibility(showBatteryWarning || showDataSaverWarning ? View.VISIBLE : View.GONE);
if (showDataSaverWarning) {
this.mDisableOsOptimizationsHeadline.setText(R.string.data_saver_enabled);
this.getmDisableOsOptimizationsBody.setText(R.string.data_saver_enabled_explained);
this.mDisableOsOptimizationsButton.setText(R.string.allow);
this.mDisableOsOptimizationsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Settings.ACTION_IGNORE_BACKGROUND_DATA_RESTRICTIONS_SETTINGS);
Uri uri = Uri.parse("package:"+getPackageName());
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_DATA_SAVER);
} catch (ActivityNotFoundException e) {
Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_data_saver, Toast.LENGTH_SHORT).show();
}
});
builder.create().show();
}
});
} else if (showBatteryWarning) {
this.mDisableOsOptimizationsButton.setText(R.string.disable);
this.mDisableOsOptimizationsHeadline.setText(R.string.battery_optimizations_enabled);
this.getmDisableOsOptimizationsBody.setText(R.string.battery_optimizations_enabled_explained);
this.mDisableOsOptimizationsButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
Uri uri = Uri.parse("package:"+getPackageName());
intent.setData(uri);
try {
startActivityForResult(intent, REQUEST_BATTERY_OP);
} catch (ActivityNotFoundException e) {
Toast.makeText(EditAccountActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
}
}
});
}
}
public void showWipePepDialog() {
@ -1119,4 +1171,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
});
}
@Override
public void OnUpdateBlocklist(Status status) {
refreshUi();
}
}

View file

@ -1,19 +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);
}
@ -125,4 +139,27 @@ public class EditMessage extends EditText {
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;
}
}
}

View file

@ -0,0 +1,288 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.widget.Switch;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.XmppUri;
public abstract class OmemoActivity extends XmppActivity {
private Account mSelectedAccount;
private String mSelectedFingerprint;
protected XmppUri mPendingFingerprintVerificationUri = null;
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu,v,menuInfo);
Object account = v.getTag(R.id.TAG_ACCOUNT);
Object fingerprint = v.getTag(R.id.TAG_FINGERPRINT);
Object fingerprintStatus = v.getTag(R.id.TAG_FINGERPRINT_STATUS);;
if (account != null
&& fingerprint != null
&& account instanceof Account
&& fingerprintStatus != null
&& fingerprint instanceof String
&& fingerprintStatus instanceof FingerprintStatus) {
getMenuInflater().inflate(R.menu.omemo_key_context, menu);
MenuItem distrust = menu.findItem(R.id.distrust_key);
MenuItem verifyScan = menu.findItem(R.id.verify_scan);
if (this instanceof TrustKeysActivity) {
distrust.setVisible(false);
verifyScan.setVisible(false);
} else {
FingerprintStatus status = (FingerprintStatus) fingerprintStatus;
if (!status.isActive() || status.isVerified()) {
verifyScan.setVisible(false);
}
distrust.setVisible(status.isVerified());
}
this.mSelectedAccount = (Account) account;
this.mSelectedFingerprint = (String) fingerprint;
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.distrust_key:
showPurgeKeyDialog(mSelectedAccount,mSelectedFingerprint);
break;
case R.id.copy_omemo_key:
copyOmemoFingerprint(mSelectedFingerprint);
break;
case R.id.verify_scan:
new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
break;
}
return true;
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent intent) {
IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
if (scanResult != null && scanResult.getFormatName() != null) {
String data = scanResult.getContents();
XmppUri uri = new XmppUri(data);
if (xmppConnectionServiceBound) {
processFingerprintVerification(uri);
} else {
this.mPendingFingerprintVerificationUri =uri;
}
}
}
protected abstract void processFingerprintVerification(XmppUri uri);
protected void copyOmemoFingerprint(String fingerprint) {
if (copyTextToClipboard(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)), R.string.omemo_fingerprint)) {
Toast.makeText(
this,
R.string.toast_message_omemo_fingerprint,
Toast.LENGTH_SHORT).show();
}
}
protected void addFingerprintRow(LinearLayout keys, final XmppAxolotlSession session, boolean highlight) {
final Account account = session.getAccount();
final String fingerprint = session.getFingerprint();
addFingerprintRowWithListeners(keys,
session.getAccount(),
session.getFingerprint(),
highlight,
session.getTrust(),
true,
true,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
account.getAxolotlService().setFingerprintTrust(fingerprint, FingerprintStatus.createActive(isChecked));
}
});
}
protected void addFingerprintRowWithListeners(LinearLayout keys, final Account account,
final String fingerprint,
boolean highlight,
FingerprintStatus status,
boolean showTag,
boolean undecidedNeedEnablement,
CompoundButton.OnCheckedChangeListener
onCheckedChangeListener) {
View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
if (Config.X509_VERIFICATION && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509) {
View.OnClickListener listener = new View.OnClickListener() {
@Override
public void onClick(View v) {
showX509Certificate(account,fingerprint);
}
};
key.setOnClickListener(listener);
keyType.setOnClickListener(listener);
}
Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
ImageView verifiedFingerprintSymbol = (ImageView) view.findViewById(R.id.verified_fingerprint);
trustToggle.setVisibility(View.VISIBLE);
registerForContextMenu(view);
view.setTag(R.id.TAG_ACCOUNT,account);
view.setTag(R.id.TAG_FINGERPRINT,fingerprint);
view.setTag(R.id.TAG_FINGERPRINT_STATUS,status);
boolean x509 = Config.X509_VERIFICATION && status.getTrust() == FingerprintStatus.Trust.VERIFIED_X509;
final View.OnClickListener toast;
trustToggle.setChecked(status.isTrusted(), false);
if (status.isActive()){
key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor());
if (status.isVerified()) {
verifiedFingerprintSymbol.setVisibility(View.VISIBLE);
verifiedFingerprintSymbol.setAlpha(1.0f);
trustToggle.setVisibility(View.GONE);
verifiedFingerprintSymbol.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceToast(getString(R.string.this_device_has_been_verified), false);
}
});
toast = null;
} else {
verifiedFingerprintSymbol.setVisibility(View.GONE);
trustToggle.setVisibility(View.VISIBLE);
trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
if (status.getTrust() == FingerprintStatus.Trust.UNDECIDED && undecidedNeedEnablement) {
trustToggle.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
account.getAxolotlService().setFingerprintTrust(fingerprint,FingerprintStatus.createActive(false));
v.setEnabled(true);
v.setOnClickListener(null);
}
});
trustToggle.setEnabled(false);
} else {
trustToggle.setOnClickListener(null);
trustToggle.setEnabled(true);
}
toast = new View.OnClickListener() {
@Override
public void onClick(View v) {
hideToast();
}
};
}
} else {
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
toast = new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceToast(getString(R.string.this_device_is_no_longer_in_use), false);
}
};
if (status.isVerified()) {
trustToggle.setVisibility(View.GONE);
verifiedFingerprintSymbol.setVisibility(View.VISIBLE);
verifiedFingerprintSymbol.setAlpha(0.4368f);
verifiedFingerprintSymbol.setOnClickListener(toast);
} else {
trustToggle.setVisibility(View.VISIBLE);
verifiedFingerprintSymbol.setVisibility(View.GONE);
trustToggle.setOnClickListener(null);
trustToggle.setEnabled(false);
trustToggle.setOnClickListener(toast);
}
}
view.setOnClickListener(toast);
key.setOnClickListener(toast);
keyType.setOnClickListener(toast);
if (showTag) {
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
} else {
keyType.setVisibility(View.GONE);
}
if (highlight) {
keyType.setTextColor(getResources().getColor(R.color.accent));
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
} else {
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
}
key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
keys.addView(view);
}
public void showPurgeKeyDialog(final Account account, final String fingerprint) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.distrust_omemo_key);
builder.setMessage(R.string.distrust_omemo_key_text);
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(R.string.confirm,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
account.getAxolotlService().distrustFingerprint(fingerprint);
refreshUi();
}
});
builder.create().show();
}
private void showX509Certificate(Account account, String fingerprint) {
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();
}
}

View file

@ -7,6 +7,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.ListPreference;
@ -14,8 +15,11 @@ import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.provider.MediaStore;
import android.util.Log;
import android.widget.Toast;
import java.io.File;
import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Arrays;
@ -35,6 +39,13 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class SettingsActivity extends XmppActivity implements
OnSharedPreferenceChangeListener {
public static final String KEEP_FOREGROUND_SERVICE = "enable_foreground_service";
public static final String AWAY_WHEN_SCREEN_IS_OFF = "away_when_screen_off";
public static final String TREAT_VIBRATE_AS_SILENT = "treat_vibrate_as_silent";
public static final String MANUALLY_CHANGE_PRESENCE = "manually_change_presence";
public static final String BLIND_TRUST_BEFORE_VERIFICATION = "btbv";
public static final String AUTOMATIC_MESSAGE_DELETION = "automatic_message_deletion";
public static final int REQUEST_WRITE_LOGS = 0xbf8701;
private SettingsFragment mSettingsFragment;
@ -83,6 +94,27 @@ public class SettingsActivity extends XmppActivity implements
}
}
boolean removeLocation = new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null;
boolean removeVoice = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null;
ListPreference quickAction = (ListPreference) mSettingsFragment.findPreference("quick_action");
if (quickAction != null && (removeLocation || removeVoice)) {
ArrayList<CharSequence> entries = new ArrayList<>(Arrays.asList(quickAction.getEntries()));
ArrayList<CharSequence> entryValues = new ArrayList<>(Arrays.asList(quickAction.getEntryValues()));
int index = entryValues.indexOf("location");
if (index > 0 && removeLocation) {
entries.remove(index);
entryValues.remove(index);
}
index = entryValues.indexOf("voice");
if (index > 0 && removeVoice) {
entries.remove(index);
entryValues.remove(index);
}
quickAction.setEntries(entries.toArray(new CharSequence[entries.size()]));
quickAction.setEntryValues(entryValues.toArray(new CharSequence[entryValues.size()]));
}
final Preference removeCertsPreference = mSettingsFragment.findPreference("remove_trusted_certificates");
removeCertsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
@ -156,6 +188,26 @@ public class SettingsActivity extends XmppActivity implements
}
});
if (Config.ONLY_INTERNAL_STORAGE) {
final Preference cleanCachePreference = mSettingsFragment.findPreference("clean_cache");
cleanCachePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
cleanCache();
return true;
}
});
final Preference cleanPrivateStoragePreference = mSettingsFragment.findPreference("clean_private_storage");
cleanPrivateStoragePreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
cleanPrivateStorage();
return true;
}
});
}
final Preference deleteOmemoPreference = mSettingsFragment.findPreference("delete_omemo_identities");
deleteOmemoPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
@ -166,6 +218,57 @@ public class SettingsActivity extends XmppActivity implements
});
}
private void cleanCache() {
Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
private void cleanPrivateStorage() {
cleanPrivatePictures();
cleanPrivateFiles();
}
private void cleanPrivatePictures() {
try {
File dir = new File(getFilesDir().getAbsolutePath(), "/Pictures/");
File[] array = dir.listFiles();
if (array != null) {
for (int b = 0; b < array.length; b++) {
String name = array[b].getName().toLowerCase();
if (name.equals(".nomedia")) {
continue;
}
if (array[b].isFile()) {
array[b].delete();
}
}
}
} catch (Throwable e) {
Log.e("CleanCache", e.toString());
}
}
private void cleanPrivateFiles() {
try {
File dir = new File(getFilesDir().getAbsolutePath(), "/Files/");
File[] array = dir.listFiles();
if (array != null) {
for (int b = 0; b < array.length; b++) {
String name = array[b].getName().toLowerCase();
if (name.equals(".nomedia")) {
continue;
}
if (array[b].isFile()) {
array[b].delete();
}
}
}
} catch (Throwable e) {
Log.e("CleanCache", e.toString());
}
}
private void deleteOmemoIdentities() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.pref_delete_omemo_identities);
@ -227,10 +330,10 @@ public class SettingsActivity extends XmppActivity implements
final List<String> resendPresence = Arrays.asList(
"confirm_messages",
"xa_on_silent_mode",
"away_when_screen_off",
AWAY_WHEN_SCREEN_IS_OFF,
"allow_message_correction",
"treat_vibrate_as_silent",
"manually_change_presence",
TREAT_VIBRATE_AS_SILENT,
MANUALLY_CHANGE_PRESENCE,
"last_activity");
if (name.equals("resource")) {
String resource = preferences.getString("resource", "mobile")
@ -248,15 +351,18 @@ public class SettingsActivity extends XmppActivity implements
}
}
}
} else if (name.equals("keep_foreground_service")) {
} else if (name.equals(KEEP_FOREGROUND_SERVICE)) {
boolean foreground_service = preferences.getBoolean(KEEP_FOREGROUND_SERVICE,false);
if (!foreground_service) {
xmppConnectionService.clearStartTimeCounter();
}
xmppConnectionService.toggleForegroundService();
} else if (resendPresence.contains(name)) {
if (xmppConnectionServiceBound) {
if (name.equals("away_when_screen_off")
|| name.equals("manually_change_presence")) {
if (name.equals(AWAY_WHEN_SCREEN_IS_OFF) || name.equals(MANUALLY_CHANGE_PRESENCE)) {
xmppConnectionService.toggleScreenEventReceiver();
}
if (name.equals("manually_change_presence") && !noAccountUsesPgp()) {
if (name.equals(MANUALLY_CHANGE_PRESENCE) && !noAccountUsesPgp()) {
Toast.makeText(this, R.string.republish_pgp_keys, Toast.LENGTH_LONG).show();
}
xmppConnectionService.refreshAllPresences();
@ -266,6 +372,8 @@ public class SettingsActivity extends XmppActivity implements
reconnectAccounts();
} else if (name.equals("use_tor")) {
reconnectAccounts();
} else if (name.equals(AUTOMATIC_MESSAGE_DELETION)) {
xmppConnectionService.expireOldMessages(true);
}
}

View file

@ -3,6 +3,7 @@ package eu.siacs.conversations.ui;
import android.app.Dialog;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceCategory;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
import android.view.View;
@ -11,6 +12,7 @@ import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
public class SettingsFragment extends PreferenceFragment {
@ -52,6 +54,16 @@ public class SettingsFragment extends PreferenceFragment {
// Load the preferences from an XML resource
addPreferencesFromResource(R.xml.preferences);
// Remove from standard preferences if the flag ONLY_INTERNAL_STORAGE is not true
if (!Config.ONLY_INTERNAL_STORAGE) {
PreferenceCategory mCategory = (PreferenceCategory) findPreference("security_options");
Preference mPref1 = findPreference("clean_cache");
Preference mPref2 = findPreference("clean_private_storage");
mCategory.removePreference(mPref1);
mCategory.removePreference(mPref2);
}
}
@Override

View file

@ -59,7 +59,17 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
private Toast mToast;
private AtomicInteger attachmentCounter = new AtomicInteger(0);
private UiCallback<Message> attachFileCallback = new UiCallback<Message>() {
private UiInformableCallback<Message> attachFileCallback = new UiInformableCallback<Message>() {
@Override
public void inform(final String text) {
runOnUiThread(new Runnable() {
@Override
public void run() {
replaceToast(text);
}
});
}
@Override
public void userInputRequried(PendingIntent pi, Message object) {
@ -293,8 +303,7 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
} else {
replaceToast(getString(R.string.preparing_file));
ShareWithActivity.this.xmppConnectionService
.attachFileToConversation(conversation, share.uris.get(0),
attachFileCallback);
.attachFileToConversation(conversation, share.uris.get(0), attachFileCallback);
}
}
};
@ -310,16 +319,62 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer
} else {
if (mReturnToPrevious && this.share.text != null && !this.share.text.isEmpty() ) {
final OnPresenceSelected callback = new OnPresenceSelected() {
@Override
public void onPresenceSelected() {
Message message = new Message(conversation,share.text, conversation.getNextEncryption());
if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
message.setCounterpart(conversation.getNextCounterpart());
}
private void finishAndSend(Message message) {
xmppConnectionService.sendMessage(message);
replaceToast(getString(R.string.shared_text_with_x, conversation.getName()));
finish();
}
private UiCallback<Message> messageEncryptionCallback = new UiCallback<Message>() {
@Override
public void success(final Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
runOnUiThread(new Runnable() {
@Override
public void run() {
finishAndSend(message);
}
});
}
@Override
public void error(final int errorCode, Message object) {
runOnUiThread(new Runnable() {
@Override
public void run() {
replaceToast(getString(errorCode));
finish();
}
});
}
@Override
public void userInputRequried(PendingIntent pi, Message object) {
finish();
}
};
@Override
public void onPresenceSelected() {
final int encryption = conversation.getNextEncryption();
Message message = new Message(conversation,share.text, encryption);
Log.d(Config.LOGTAG,"on presence selected encrpytion="+encryption);
if (encryption == Message.ENCRYPTION_PGP) {
replaceToast(getString(R.string.encrypting_message));
xmppConnectionService.getPgpEngine().encrypt(message,messageEncryptionCallback);
return;
}
if (encryption == Message.ENCRYPTION_OTR) {
message.setCounterpart(conversation.getNextCounterpart());
}
finishAndSend(message);
}
};
if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) {
selectPresence(conversation, callback);

View file

@ -13,6 +13,7 @@ import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.ListFragment;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
@ -25,12 +26,13 @@ import android.nfc.NfcAdapter;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v13.app.FragmentPagerAdapter;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.text.Editable;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.Log;
import android.text.style.TypefaceSpan;
import android.util.Pair;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@ -65,7 +67,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -324,6 +325,23 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
openConversationsForBookmark(bookmark);
}
protected void shareBookmarkUri() {
shareBookmarkUri(conference_context_id);
}
protected void shareBookmarkUri(int position) {
Bookmark bookmark = (Bookmark) conferences.get(position);
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, "xmpp:"+bookmark.getJid().toBareJid().toString()+"?join");
shareIntent.setType("text/plain");
try {
startActivity(Intent.createChooser(shareIntent, getText(R.string.share_uri_with)));
} catch (ActivityNotFoundException e) {
Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
}
}
protected void openConversationsForBookmark(Bookmark bookmark) {
Jid jid = bookmark.getJid();
if (jid == null) {
@ -397,11 +415,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
@SuppressLint("InflateParams")
protected void showCreateContactDialog(final String prefilledJid, final String fingerprint) {
protected void showCreateContactDialog(final String prefilledJid, final Invite invite) {
EnterJidDialog dialog = new EnterJidDialog(
this, mKnownHosts, mActivatedAccounts,
getString(R.string.create_contact), getString(R.string.create),
prefilledJid, null, fingerprint == null
prefilledJid, null, invite == null || !invite.hasFingerprints()
);
dialog.setOnEnterJidDialogPositiveListener(new EnterJidDialog.OnEnterJidDialogPositiveListener() {
@ -420,9 +438,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
if (contact.showInRoster()) {
throw new EnterJidDialog.JidError(getString(R.string.contact_already_exists));
} else {
contact.addOtrFingerprint(fingerprint);
xmppConnectionService.createContact(contact);
switchToConversation(contact);
if (invite != null && invite.hasFingerprints()) {
xmppConnectionService.verifyFingerprints(contact,invite.getFingerprints());
}
switchToConversation(contact, invite == null ? null : invite.getBody());
return true;
}
}
@ -561,11 +581,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
return xmppConnectionService.findAccountByJid(jid);
}
protected void switchToConversation(Contact contact) {
protected void switchToConversation(Contact contact, String body) {
Conversation conversation = xmppConnectionService
.findOrCreateConversation(contact.getAccount(),
contact.getJid(), false);
switchToConversation(conversation);
switchToConversation(conversation, body, false);
}
public static void populateAccountSpinner(Context context, List<String> accounts, Spinner spinner) {
@ -624,7 +644,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
showCreateConferenceDialog();
return true;
case R.id.action_scan_qr_code:
new IntentIntegrator(this).initiateScan();
new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
return true;
case R.id.action_hide_offline:
mHideOfflineContacts = !item.isChecked();
@ -786,12 +806,15 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
if (this.mPendingInvite != null) {
mPendingInvite.invite();
this.mPendingInvite = null;
filter(null);
} else if (!handleIntent(getIntent())) {
if (mSearchEditText != null) {
filter(mSearchEditText.getText().toString());
} else {
filter(null);
}
} else {
filter(null);
}
setIntent(null);
}
@ -810,15 +833,13 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
case Intent.ACTION_VIEW:
Uri uri = intent.getData();
if (uri != null) {
Log.d(Config.LOGTAG, "received uri=" + intent.getData());
return new Invite(intent.getData()).invite();
return new Invite(intent.getData(),false).invite();
} else {
return false;
}
case NfcAdapter.ACTION_NDEF_DISCOVERED:
for (Parcelable message : getIntent().getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)) {
if (message instanceof NdefMessage) {
Log.d(Config.LOGTAG, "received message=" + message);
for (NdefRecord record : ((NdefMessage) message).getRecords()) {
switch (record.getTnf()) {
case NdefRecord.TNF_WELL_KNOWN:
@ -842,28 +863,40 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
private boolean handleJid(Invite invite) {
Account account = xmppConnectionService.findAccountByJid(invite.getJid());
if (account != null && !account.isOptionSet(Account.OPTION_DISABLED)) {
if (invite.hasFingerprints() && xmppConnectionService.verifyFingerprints(account,invite.getFingerprints())) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
switchToAccount(account);
finish();
return true;
}
List<Contact> contacts = xmppConnectionService.findContacts(invite.getJid());
if (invite.isMuc()) {
Conversation muc = xmppConnectionService.findFirstMuc(invite.getJid());
if (muc != null) {
switchToConversation(muc);
switchToConversation(muc,invite.getBody(),false);
return true;
} else {
showJoinConferenceDialog(invite.getJid().toBareJid().toString());
return false;
}
} else if (contacts.size() == 0) {
showCreateContactDialog(invite.getJid().toString(), invite.getFingerprint());
showCreateContactDialog(invite.getJid().toString(), invite);
return false;
} else if (contacts.size() == 1) {
Contact contact = contacts.get(0);
if (invite.getFingerprint() != null) {
if (contact.addOtrFingerprint(invite.getFingerprint())) {
Log.d(Config.LOGTAG, "added new fingerprint");
xmppConnectionService.syncRosterToDisk(contact.getAccount());
if (!invite.isSafeSource() && invite.hasFingerprints()) {
displayVerificationWarningDialog(contact,invite);
} else {
if (invite.hasFingerprints()) {
if(xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints())) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
}
switchToConversation(contact, invite.getBody());
}
switchToConversation(contact);
return true;
} else {
if (mMenuSearchView != null) {
@ -878,6 +911,46 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
}
private void displayVerificationWarningDialog(final Contact contact, final Invite invite) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.verify_omemo_keys);
View view = getLayoutInflater().inflate(R.layout.dialog_verify_fingerprints, null);
final CheckBox isTrustedSource = (CheckBox) view.findViewById(R.id.trusted_source);
TextView warning = (TextView) view.findViewById(R.id.warning);
String jid = contact.getJid().toBareJid().toString();
SpannableString spannable = new SpannableString(getString(R.string.verifying_omemo_keys_trusted_source,jid,contact.getDisplayName()));
int start = spannable.toString().indexOf(jid);
if (start >= 0) {
spannable.setSpan(new TypefaceSpan("monospace"),start,start + jid.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
warning.setText(spannable);
builder.setView(view);
builder.setPositiveButton(R.string.confirm, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (isTrustedSource.isChecked() && invite.hasFingerprints()) {
xmppConnectionService.verifyFingerprints(contact, invite.getFingerprints());
}
switchToConversation(contact, invite.getBody());
}
});
builder.setNegativeButton(R.string.cancel, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
StartConversationActivity.this.finish();
}
});
AlertDialog dialog = builder.create();
dialog.setCanceledOnTouchOutside(false);
dialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
StartConversationActivity.this.finish();
}
});
dialog.show();
}
protected void filter(String needle) {
if (xmppConnectionServiceBound) {
this.filterContacts(needle);
@ -1049,10 +1122,14 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
activity.conference_context_id = acmi.position;
} else if (mResContextMenu == R.menu.contact_context) {
activity.contact_context_id = acmi.position;
final Blockable contact = (Contact) activity.contacts.get(acmi.position);
final Contact contact = (Contact) activity.contacts.get(acmi.position);
final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock);
final MenuItem showContactDetailsItem = menu.findItem(R.id.context_contact_details);
if (contact.isSelf()) {
showContactDetailsItem.setVisible(false);
}
XmppConnection xmpp = contact.getAccount().getXmppConnection();
if (xmpp != null && xmpp.getFeatures().blocking()) {
if (xmpp != null && xmpp.getFeatures().blocking() && !contact.isSelf()) {
if (contact.isBlocked()) {
blockUnblockItem.setTitle(R.string.unblock_contact);
} else {
@ -1083,6 +1160,9 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
case R.id.context_join_conference:
activity.openConversationForBookmark();
break;
case R.id.context_share_uri:
activity.shareBookmarkUri();
break;
case R.id.context_delete_conference:
activity.deleteConference();
}
@ -1100,6 +1180,10 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
super(uri);
}
public Invite(Uri uri, boolean safeSource) {
super(uri,safeSource);
}
boolean invite() {
if (getJid() != null) {
return handleJid(this);

View file

@ -1,7 +1,12 @@
package eu.siacs.conversations.ui;
import android.app.ActionBar;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
@ -10,24 +15,29 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.integration.android.IntentIntegrator;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
public class TrustKeysActivity extends OmemoActivity implements OnKeyStatusUpdated {
private List<Jid> contactJids;
private Account mAccount;
@ -61,6 +71,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
finish();
}
};
private Toast mUseCameraHintToast = null;
@Override
protected void refreshUiReal() {
@ -99,6 +110,61 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.trust_keys, menu);
mUseCameraHintToast = Toast.makeText(this,R.string.use_camera_icon_to_scan_barcode,Toast.LENGTH_LONG);
ActionBar actionBar = getActionBar();
mUseCameraHintToast.setGravity(Gravity.TOP | Gravity.END, 0 ,actionBar == null ? 0 : actionBar.getHeight());
mUseCameraHintToast.show();
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_scan_qr_code:
if (hasPendingKeyFetches()) {
Toast.makeText(this, R.string.please_wait_for_keys_to_be_fetched, Toast.LENGTH_SHORT).show();
} else {
new IntentIntegrator(this).initiateScan(Arrays.asList("AZTEC","QR_CODE"));
return true;
}
}
return super.onOptionsItemSelected(item);
}
@Override
protected void onStop() {
super.onStop();
if (mUseCameraHintToast != null) {
mUseCameraHintToast.cancel();
}
}
@Override
protected void processFingerprintVerification(XmppUri uri) {
if (mConversation != null
&& mAccount != null
&& uri.hasFingerprints()
&& mAccount.getAxolotlService().getCryptoTargets(mConversation).contains(uri.getJid())) {
boolean performedVerification = xmppConnectionService.verifyFingerprints(mAccount.getRoster().getContact(uri.getJid()),uri.getFingerprints());
boolean keys = reloadFingerprints();
if (performedVerification && !keys && !hasNoOtherTrustedKeys() && !hasPendingKeyFetches()) {
Toast.makeText(this,R.string.all_omemo_keys_have_been_verified, Toast.LENGTH_SHORT).show();
finishOk();
return;
} else if (performedVerification) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
} else {
reloadFingerprints();
Log.d(Config.LOGTAG,"xmpp uri was: "+uri.getJid()+" has Fingerprints: "+Boolean.toString(uri.hasFingerprints()));
Toast.makeText(this,R.string.barcode_does_not_contain_fingerprints_for_this_conversation,Toast.LENGTH_SHORT).show();
}
populateView();
}
private void populateView() {
setTitle(getString(R.string.trust_omemo_fingerprints));
ownKeys.removeAllViews();
@ -108,16 +174,14 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
for(final String fingerprint : ownKeysToTrust.keySet()) {
hasOwnKeys = true;
addFingerprintRowWithListeners(ownKeys, mAccount, fingerprint, false,
XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)), false,
FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)), false, false,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ownKeysToTrust.put(fingerprint, isChecked);
// own fingerprints have no impact on locked status.
}
},
null,
null
}
);
}
@ -133,16 +197,14 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
final Map<String, Boolean> fingerprints = entry.getValue();
for (final String fingerprint : fingerprints.keySet()) {
addFingerprintRowWithListeners(keysContainer, mAccount, fingerprint, false,
XmppAxolotlSession.Trust.fromBoolean(fingerprints.get(fingerprint)), false,
FingerprintStatus.createActive(fingerprints.get(fingerprint)), false, false,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
fingerprints.put(fingerprint, isChecked);
lockOrUnlockAsNeeded();
}
},
null,
null
}
);
}
if (fingerprints.size() == 0) {
@ -184,7 +246,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
ownKeysToTrust.clear();
AxolotlService service = this.mAccount.getAxolotlService();
Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
Set<IdentityKey> ownKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided());
for(final IdentityKey identityKey : ownKeysSet) {
if(!ownKeysToTrust.containsKey(identityKey)) {
ownKeysToTrust.put(identityKey.getFingerprint().replaceAll("\\s", ""), false);
@ -193,9 +255,9 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
synchronized (this.foreignKeysToTrust) {
foreignKeysToTrust.clear();
for (Jid jid : contactJids) {
Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, jid);
Set<IdentityKey> foreignKeysSet = service.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), jid);
if (hasNoOtherTrustedKeys(jid) && ownKeysSet.size() == 0) {
foreignKeysSet.addAll(service.getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, jid));
foreignKeysSet.addAll(service.getKeysWithTrust(FingerprintStatus.createActive(false), jid));
}
Map<String, Boolean> foreignFingerprints = new HashMap<>();
for (final IdentityKey identityKey : foreignKeysSet) {
@ -211,15 +273,19 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
return ownKeysSet.size() + foreignKeysToTrust.size() > 0;
}
@Override
public void onBackendConnected() {
Intent intent = getIntent();
this.mAccount = extractAccount(intent);
if (this.mAccount != null && intent != null) {
String uuid = intent.getStringExtra("conversation");
this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
reloadFingerprints();
populateView();
if (this.mPendingFingerprintVerificationUri != null) {
processFingerprintVerification(this.mPendingFingerprintVerificationUri);
this.mPendingFingerprintVerificationUri = null;
} else {
reloadFingerprints();
populateView();
}
}
}
@ -238,24 +304,32 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
@Override
public void onKeyStatusUpdated(final AxolotlService.FetchStatus report) {
final boolean keysToTrust = reloadFingerprints();
if (report != null) {
lastFetchReport = report;
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mUseCameraHintToast != null && !keysToTrust) {
mUseCameraHintToast.cancel();
}
switch (report) {
case ERROR:
Toast.makeText(TrustKeysActivity.this,R.string.error_fetching_omemo_key,Toast.LENGTH_SHORT).show();
break;
case SUCCESS_TRUSTED:
Toast.makeText(TrustKeysActivity.this,R.string.blindly_trusted_omemo_keys,Toast.LENGTH_LONG).show();
break;
case SUCCESS_VERIFIED:
Toast.makeText(TrustKeysActivity.this,R.string.verified_omemo_key_with_certificate,Toast.LENGTH_LONG).show();
Toast.makeText(TrustKeysActivity.this,
Config.X509_VERIFICATION ? R.string.verified_omemo_key_with_certificate : R.string.all_omemo_keys_have_been_verified,
Toast.LENGTH_LONG).show();
break;
}
}
});
}
boolean keysToTrust = reloadFingerprints();
if (keysToTrust || hasPendingKeyFetches() || hasNoOtherTrustedKeys()) {
refreshUi();
} else {
@ -280,7 +354,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
for(final String fingerprint :ownKeysToTrust.keySet()) {
mAccount.getAxolotlService().setFingerprintTrust(
fingerprint,
XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(fingerprint)));
FingerprintStatus.createActive(ownKeysToTrust.get(fingerprint)));
}
List<Jid> acceptedTargets = mConversation == null ? new ArrayList<Jid>() : mConversation.getAcceptedCryptoTargets();
synchronized (this.foreignKeysToTrust) {
@ -293,7 +367,7 @@ public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdate
for (final String fingerprint : value.keySet()) {
mAccount.getAxolotlService().setFingerprintTrust(
fingerprint,
XmppAxolotlSession.Trust.fromBoolean(value.get(fingerprint)));
FingerprintStatus.createActive(value.get(fingerprint)));
}
}
}

View file

@ -0,0 +1,5 @@
package eu.siacs.conversations.ui;
public interface UiInformableCallback<T> extends UiCallback<T> {
void inform(String text);
}

View file

@ -173,11 +173,10 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer
protected boolean verifyWithUri(XmppUri uri) {
Contact contact = mConversation.getContact();
if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) {
contact.addOtrFingerprint(uri.getFingerprint());
if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.hasFingerprints()) {
xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints());
Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show();
updateView();
xmppConnectionService.syncRosterToDisk(contact.getAccount());
return true;
} else {
Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show();

View file

@ -8,22 +8,35 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import eu.siacs.conversations.R;
import java.util.List;
public class WelcomeActivity extends Activity {
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
public class WelcomeActivity extends XmppActivity {
@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.welcome);
final ActionBar ab = getActionBar();
if (ab != null) {
ab.setDisplayShowHomeEnabled(false);
ab.setDisplayHomeAsUpEnabled(false);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.welcome);
final Button createAccount = (Button) findViewById(R.id.create_account);
createAccount.setOnClickListener(new View.OnClickListener() {
@Override
@ -37,7 +50,15 @@ public class WelcomeActivity extends Activity {
useOwnProvider.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(WelcomeActivity.this, EditAccountActivity.class));
List<Account> accounts = xmppConnectionService.getAccounts();
Intent intent = new Intent(WelcomeActivity.this, EditAccountActivity.class);
if (accounts.size() == 1) {
intent.putExtra("jid",accounts.get(0).getJid().toBareJid().toString());
intent.putExtra("init",true);
} else if (accounts.size() >= 1) {
intent = new Intent(WelcomeActivity.this, ManageAccountActivity.class);
}
startActivity(intent);
}
});

View file

@ -28,6 +28,7 @@ import android.graphics.Color;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.nfc.NdefRecord;
@ -43,24 +44,18 @@ import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Pair;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.WriterException;
import com.google.zxing.aztec.AztecWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.java.otr4j.session.SessionID;
@ -68,7 +63,6 @@ import java.io.FileNotFoundException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
@ -77,19 +71,16 @@ import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
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.MucOptions;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.BarcodeProvider;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.ui.widget.Switch;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.utils.UIHelper;
@ -446,6 +437,16 @@ public abstract class XmppActivity extends Activity {
}
}
protected boolean isAffectedByDataSaver() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
return cm.isActiveNetworkMetered()
&& cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
} else {
return false;
}
}
protected boolean usingEnterKey() {
return getPreferences().getBoolean("display_enter_key", false);
}
@ -768,164 +769,6 @@ public abstract class XmppActivity extends Activity {
builder.create().show();
}
protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
final XmppAxolotlSession.Trust trust = account.getAxolotlService()
.getFingerprintTrust(fingerprint);
if (trust == null) {
return false;
}
return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
account.getAxolotlService().setFingerprintTrust(fingerprint,
(isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
XmppAxolotlSession.Trust.UNTRUSTED);
}
},
new View.OnClickListener() {
@Override
public void onClick(View v) {
account.getAxolotlService().setFingerprintTrust(fingerprint,
XmppAxolotlSession.Trust.UNTRUSTED);
v.setEnabled(true);
}
},
onKeyClickedListener
);
}
protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
final String fingerprint,
boolean highlight,
XmppAxolotlSession.Trust trust,
boolean showTag,
CompoundButton.OnCheckedChangeListener
onCheckedChangeListener,
View.OnClickListener onClickListener,
View.OnClickListener onKeyClickedListener) {
if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
return false;
}
View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
key.setOnClickListener(onKeyClickedListener);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
keyType.setOnClickListener(onKeyClickedListener);
Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
trustToggle.setVisibility(View.VISIBLE);
trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
trustToggle.setOnClickListener(onClickListener);
final View.OnLongClickListener purge = new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showPurgeKeyDialog(account, fingerprint);
return true;
}
};
boolean active = true;
view.setOnLongClickListener(purge);
key.setOnLongClickListener(purge);
keyType.setOnLongClickListener(purge);
boolean x509 = Config.X509_VERIFICATION
&& (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
switch (trust) {
case UNTRUSTED:
case TRUSTED:
case TRUSTED_X509:
trustToggle.setChecked(trust.trusted(), false);
trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
trustToggle.setOnClickListener(null);
}
key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor());
break;
case UNDECIDED:
trustToggle.setChecked(false, false);
trustToggle.setEnabled(false);
key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor());
break;
case INACTIVE_UNTRUSTED:
case INACTIVE_UNDECIDED:
trustToggle.setOnClickListener(null);
trustToggle.setChecked(false, false);
trustToggle.setEnabled(false);
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
active = false;
break;
case INACTIVE_TRUSTED:
case INACTIVE_TRUSTED_X509:
trustToggle.setOnClickListener(null);
trustToggle.setChecked(true, false);
trustToggle.setEnabled(false);
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
active = false;
break;
}
if (showTag) {
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
} else {
keyType.setVisibility(View.GONE);
}
if (highlight) {
keyType.setTextColor(getResources().getColor(R.color.accent));
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
} else {
keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
}
key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
final View.OnClickListener toast;
if (!active) {
toast = new View.OnClickListener() {
@Override
public void onClick(View v) {
replaceToast(getString(R.string.this_device_is_no_longer_in_use), false);
}
};
trustToggle.setOnClickListener(toast);
} else {
toast = new View.OnClickListener() {
@Override
public void onClick(View v) {
hideToast();
}
};
}
view.setOnClickListener(toast);
key.setOnClickListener(toast);
keyType.setOnClickListener(toast);
keys.addView(view);
return true;
}
public void showPurgeKeyDialog(final Account account, final String fingerprint) {
Builder builder = new Builder(this);
builder.setTitle(getString(R.string.purge_key));
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage(getString(R.string.purge_key_desc_part1)
+ "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
+ "\n\n" + getString(R.string.purge_key_desc_part2));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.purge_key),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
account.getAxolotlService().purgeKey(fingerprint);
refreshUi();
}
});
builder.create().show();
}
public boolean hasStoragePermission(int requestCode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
@ -1149,7 +992,7 @@ public abstract class XmppActivity extends Activity {
}
protected boolean manuallyChangePresence() {
return getPreferences().getBoolean("manually_change_presence", false);
return getPreferences().getBoolean(SettingsActivity.MANUALLY_CHANGE_PRESENCE, false);
}
protected void unregisterNdefPushMessageCallback() {
@ -1216,7 +1059,7 @@ public abstract class XmppActivity extends Activity {
Point size = new Point();
getWindowManager().getDefaultDisplay().getSize(size);
final int width = (size.x < size.y ? size.x : size.y);
Bitmap bitmap = createQrCodeBitmap(uri, width);
Bitmap bitmap = BarcodeProvider.createAztecBitmap(uri, width);
ImageView view = new ImageView(this);
view.setBackgroundColor(Color.WHITE);
view.setImageBitmap(bitmap);
@ -1226,31 +1069,6 @@ public abstract class XmppActivity extends Activity {
}
}
protected Bitmap createQrCodeBitmap(String input, int size) {
Log.d(Config.LOGTAG,"qr code requested size: "+size);
try {
final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, 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.TRANSPARENT;
}
}
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
} catch (final WriterException e) {
return null;
}
}
protected Account extractAccount(Intent intent) {
String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
try {

View file

@ -11,8 +11,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.support.v4.content.FileProvider;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@ -22,7 +20,9 @@ import android.text.style.RelativeSizeSpan;
import android.text.style.StyleSpan;
import android.text.util.Linkify;
import android.util.DisplayMetrics;
import android.util.Patterns;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLongClickListener;
@ -37,14 +37,14 @@ import android.widget.Toast;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.RejectedExecutionException;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
@ -52,12 +52,17 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.NotificationService;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.text.DividerSpan;
import eu.siacs.conversations.ui.text.QuoteSpan;
import eu.siacs.conversations.ui.widget.ClickableMovementMethod;
import eu.siacs.conversations.ui.widget.CopyTextView;
import eu.siacs.conversations.ui.widget.ListSelectionManager;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.Patterns;
import eu.siacs.conversations.utils.UIHelper;
public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextView.CopyHandler {
@ -71,6 +76,27 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
+ "\\;\\/\\?\\@\\&\\=\\#\\~\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])"
+ "|(?:\\%[a-fA-F0-9]{2}))+");
private static final Linkify.TransformFilter WEBURL_TRANSFORM_FILTER = new Linkify.TransformFilter() {
@Override
public String transformUrl(Matcher matcher, String url) {
if (url == null) {
return null;
}
final String lcUrl = url.toLowerCase(Locale.US);
if (lcUrl.startsWith("http://") || lcUrl.startsWith("https://")) {
return url;
} else {
return "http://"+url;
}
}
};
private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = new Linkify.MatchFilter() {
@Override
public boolean acceptMatch(CharSequence charSequence, int start, int end) {
return start < 1 || charSequence.charAt(start-1) != '@';
}
};
private ConversationActivity activity;
private DisplayMetrics metrics;
@ -81,6 +107,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
private boolean mIndicateReceived = false;
private boolean mUseGreenBackground = false;
private OnQuoteListener onQuoteListener;
private final ListSelectionManager listSelectionManager = new ListSelectionManager();
public MessageAdapter(ConversationActivity activity, List<Message> messages) {
@ -99,6 +127,10 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
this.mOnContactPictureLongClickedListener = listener;
}
public void setOnQuoteListener(OnQuoteListener listener) {
this.onQuoteListener = listener;
}
@Override
public int getViewTypeCount() {
return 3;
@ -127,7 +159,7 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
}
}
private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground) {
private void displayStatus(ViewHolder viewHolder, Message message, int type, boolean darkBackground, boolean inValidSession) {
String filesize = null;
String info = null;
boolean error = false;
@ -200,32 +232,26 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (message.getEncryption() == Message.ENCRYPTION_NONE) {
viewHolder.indicator.setVisibility(View.GONE);
} else {
viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
viewHolder.indicator.setVisibility(View.VISIBLE);
boolean verified = false;
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
XmppAxolotlSession.Trust trust = message.getConversation()
final FingerprintStatus status = message.getConversation()
.getAccount().getAxolotlService().getFingerprintTrust(
message.getFingerprint());
if(trust == null || (!trust.trusted() && !trust.trustedInactive())) {
viewHolder.indicator.setColorFilter(activity.getWarningTextColor());
viewHolder.indicator.setAlpha(1.0f);
} else {
viewHolder.indicator.clearColorFilter();
if (darkBackground) {
viewHolder.indicator.setAlpha(0.7f);
} else {
viewHolder.indicator.setAlpha(0.57f);
}
}
} else {
viewHolder.indicator.clearColorFilter();
if (darkBackground) {
viewHolder.indicator.setAlpha(0.7f);
} else {
viewHolder.indicator.setAlpha(0.57f);
if (status != null && status.isVerified()) {
verified = true;
}
}
if (verified) {
viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_verified_user_white_18dp : R.drawable.ic_verified_user_black_18dp);
} else {
viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp);
}
if (darkBackground) {
viewHolder.indicator.setAlpha(0.7f);
} else {
viewHolder.indicator.setAlpha(0.57f);
}
viewHolder.indicator.setVisibility(View.VISIBLE);
}
String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
@ -291,10 +317,78 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
viewHolder.messageBody.setIncludeFontPadding(false);
Spannable span = new SpannableString(body);
span.setSpan(new RelativeSizeSpan(4.0f), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
viewHolder.messageBody.setText(span);
}
private int applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) {
if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
body.insert(start++, "\n");
body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
end++;
}
if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
body.insert(end, "\n");
body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
int color = darkBackground ? this.getMessageTextColor(darkBackground, false)
: getContext().getResources().getColor(R.color.bubble);
DisplayMetrics metrics = getContext().getResources().getDisplayMetrics();
body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
return 0;
}
/**
* Applies QuoteSpan to group of lines which starts with > or » characters.
* Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
*/
private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
boolean startsWithQuote = false;
char previous = '\n';
int lineStart = -1;
int lineTextStart = -1;
int quoteStart = -1;
for (int i = 0; i <= body.length(); i++) {
char current = body.length() > i ? body.charAt(i) : '\n';
if (lineStart == -1) {
if (previous == '\n') {
if ((current == '>' && !UIHelper.isPositionFollowedByNumber(body,i)) || current == '\u00bb') {
// Line start with quote
lineStart = i;
if (quoteStart == -1) quoteStart = i;
if (i == 0) startsWithQuote = true;
} else if (quoteStart >= 0) {
// Line start without quote, apply spans there
applyQuoteSpan(body, quoteStart, i - 1, darkBackground);
quoteStart = -1;
}
}
} else {
// Remove extra spaces between > and first character in the line
// > character will be removed too
if (current != ' ' && lineTextStart == -1) {
lineTextStart = i;
}
if (current == '\n') {
body.delete(lineStart, lineTextStart);
i -= lineTextStart - lineStart;
if (i == lineStart) {
// Avoid empty lines because span over empty line can be hidden
body.insert(i++, " ");
}
lineStart = -1;
lineTextStart = -1;
}
}
previous = current;
}
if (quoteStart >= 0) {
// Apply spans to finishing open quote
applyQuoteSpan(body, quoteStart, body.length(), darkBackground);
}
return startsWithQuote;
}
private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground, int type) {
if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE);
@ -317,8 +411,9 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
for (Message.MergeSeparator mergeSeparator : mergeSeparators) {
int start = body.getSpanStart(mergeSeparator);
int end = body.getSpanEnd(mergeSeparator);
body.setSpan(new RelativeSizeSpan(0.3f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
boolean startsWithQuote = handleTextQuotes(body, darkBackground);
if (message.getType() != Message.TYPE_PRIVATE) {
if (hasMeCommand) {
body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(),
@ -339,7 +434,13 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
}
body.insert(0, privateMarker);
int privateMarkerIndex = privateMarker.length();
body.insert(privateMarkerIndex, " ");
if (startsWithQuote) {
body.insert(privateMarkerIndex, "\n\n");
body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
} else {
body.insert(privateMarkerIndex, " ");
}
body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)),
0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
body.setSpan(new StyleSpan(Typeface.BOLD),
@ -349,8 +450,15 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
Linkify.addLinks(body, Linkify.WEB_URLS);
if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) {
Pattern pattern = NotificationService.generateNickHighlightPattern(message.getConversation().getMucOptions().getActualNick());
Matcher matcher = pattern.matcher(body);
while(matcher.find()) {
body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
Linkify.addLinks(body, XMPP_PATTERN, "xmpp");
Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER);
Linkify.addLinks(body, GeoHelper.GEO_URI, "geo");
viewHolder.messageBody.setAutoLinkMask(0);
viewHolder.messageBody.setText(body);
@ -363,7 +471,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
}
viewHolder.messageBody.setTextColor(this.getMessageTextColor(darkBackground, true));
viewHolder.messageBody.setLinkTextColor(this.getMessageTextColor(darkBackground, true));
viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground ? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
viewHolder.messageBody.setHighlightColor(activity.getResources().getColor(darkBackground
? (type == SENT || !mUseGreenBackground ? R.color.black26 : R.color.grey800) : R.color.grey500));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
}
@ -456,15 +565,20 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
if (timestamp == 0) {
timestamp = System.currentTimeMillis();
}
activity.setMessagesLoaded();
activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp);
Toast.makeText(activity, R.string.fetching_history_from_server,Toast.LENGTH_LONG).show();
conversation.messagesLoaded.set(true);
MessageArchiveService.Query query = activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp);
if (query != null) {
Toast.makeText(activity, R.string.fetching_history_from_server, Toast.LENGTH_LONG).show();
} else {
Toast.makeText(activity,R.string.not_fetching_history_retention_period, Toast.LENGTH_SHORT).show();
}
}
@Override
public View getView(int position, View view, ViewGroup parent) {
final Message message = getItem(position);
final boolean isInValidSession = message.isValidInSession();
final boolean omemoEncryption = message.getEncryption() == Message.ENCRYPTION_AXOLOTL;
final boolean isInValidSession = message.isValidInSession() && (!omemoEncryption || message.isTrusted());
final Conversation conversation = message.getConversation();
final Account account = conversation.getAccount();
final int type = getItemViewType(position);
@ -526,7 +640,8 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
break;
}
if (viewHolder.messageBody != null) {
listSelectionManager.onCreate(viewHolder.messageBody);
listSelectionManager.onCreate(viewHolder.messageBody,
new MessageBodyActionModeCallback(viewHolder.messageBody));
viewHolder.messageBody.setCopyHandler(this);
}
view.setTag(viewHolder);
@ -670,11 +785,15 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
} else {
viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
viewHolder.encryption.setVisibility(View.VISIBLE);
viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
if (omemoEncryption && !message.isTrusted()) {
viewHolder.encryption.setText(R.string.not_trusted);
} else {
viewHolder.encryption.setText(CryptoHelper.encryptionTypeToText(message.getEncryption()));
}
}
}
displayStatus(viewHolder, message, type, darkBackground);
displayStatus(viewHolder, message, type, darkBackground, isInValidSession);
return view;
}
@ -686,9 +805,84 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
listSelectionManager.onAfterNotifyDataSetChanged();
}
private String transformText(CharSequence text, int start, int end, boolean forCopy) {
SpannableStringBuilder builder = new SpannableStringBuilder(text);
Object copySpan = new Object();
builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class);
for (DividerSpan dividerSpan : dividerSpans) {
builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan),
dividerSpan.isLarge() ? "\n\n" : "\n");
}
start = builder.getSpanStart(copySpan);
end = builder.getSpanEnd(copySpan);
if (start == -1 || end == -1) return "";
builder = new SpannableStringBuilder(builder, start, end);
if (forCopy) {
QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class);
for (QuoteSpan quoteSpan : quoteSpans) {
builder.insert(builder.getSpanStart(quoteSpan), "> ");
}
}
return builder.toString();
}
@Override
public String transformTextForCopy(CharSequence text, int start, int end) {
return text.toString().substring(start, end);
if (text instanceof Spanned) {
return transformText(text, start, end, true);
} else {
return text.toString().substring(start, end);
}
}
public interface OnQuoteListener {
public void onQuote(String text);
}
private class MessageBodyActionModeCallback implements ActionMode.Callback {
private final TextView textView;
public MessageBodyActionModeCallback(TextView textView) {
this.textView = textView;
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
if (onQuoteListener != null) {
int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply);
// 3rd item is placed after "copy" item
menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
}
return false;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (item.getItemId() == android.R.id.button1) {
int start = textView.getSelectionStart();
int end = textView.getSelectionEnd();
if (end > start) {
String text = transformText(textView.getText(), start, end, false);
if (onQuoteListener != null) {
onQuoteListener.onQuote(text);
}
mode.finish();
}
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {}
}
public void openDownloadable(Message message) {
@ -703,23 +897,18 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie
mime = "*/*";
}
Uri uri;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
uri = FileProvider.getUriForFile(activity, FileBackend.CONVERSATIONS_FILE_PROVIDER, file);
} catch (IllegalArgumentException e) {
Toast.makeText(activity,activity.getString(R.string.no_permission_to_access_x,file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
return;
}
openIntent.setDataAndType(uri, mime);
openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
} else {
uri = Uri.fromFile(file);
try {
uri = FileBackend.getUriForFile(activity, file);
} catch (SecurityException e) {
Toast.makeText(activity, activity.getString(R.string.no_permission_to_access_x, file.getAbsolutePath()), Toast.LENGTH_SHORT).show();
return;
}
openIntent.setDataAndType(uri, mime);
openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
PackageManager manager = activity.getPackageManager();
List<ResolveInfo> info = manager.queryIntentActivities(openIntent, 0);
if (info.size() == 0) {
openIntent.setDataAndType(Uri.fromFile(file),"*/*");
openIntent.setDataAndType(uri,"*/*");
}
try {
getContext().startActivity(openIntent);

View file

@ -0,0 +1,29 @@
package eu.siacs.conversations.ui.text;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
public class DividerSpan extends MetricAffectingSpan {
private static final float PROPORTION = 0.3f;
private final boolean large;
public DividerSpan(boolean large) {
this.large = large;
}
public boolean isLarge() {
return large;
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setTextSize(tp.getTextSize() * PROPORTION);
}
@Override
public void updateMeasureState(TextPaint p) {
p.setTextSize(p.getTextSize() * PROPORTION);
}
}

View file

@ -0,0 +1,52 @@
package eu.siacs.conversations.ui.text;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.text.Layout;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
import android.text.style.LeadingMarginSpan;
import android.util.DisplayMetrics;
import android.util.TypedValue;
public class QuoteSpan extends CharacterStyle implements LeadingMarginSpan {
private final int color;
private final int width;
private final int paddingLeft;
private final int paddingRight;
private static final float WIDTH_SP = 2f;
private static final float PADDING_LEFT_SP = 1.5f;
private static final float PADDING_RIGHT_SP = 8f;
public QuoteSpan(int color, DisplayMetrics metrics) {
this.color = color;
this.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, WIDTH_SP, metrics);
this.paddingLeft = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_LEFT_SP, metrics);
this.paddingRight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_RIGHT_SP, metrics);
}
@Override
public void updateDrawState(TextPaint tp) {
tp.setColor(this.color);
}
@Override
public int getLeadingMargin(boolean first) {
return paddingLeft + width + paddingRight;
}
@Override
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom,
CharSequence text, int start, int end, boolean first, Layout layout) {
Paint.Style style = p.getStyle();
int color = p.getColor();
p.setStyle(Paint.Style.FILL);
p.setColor(this.color);
c.drawRect(x + dir * paddingLeft, top, x + dir * (paddingLeft + width), bottom, p);
p.setStyle(style);
p.setColor(color);
}
}

View file

@ -69,8 +69,8 @@ public class ListSelectionManager {
private int futureSelectionStart;
private int futureSelectionEnd;
public void onCreate(TextView textView) {
final CustomCallback callback = new CustomCallback(textView);
public void onCreate(TextView textView, ActionMode.Callback additionalCallback) {
final CustomCallback callback = new CustomCallback(textView, additionalCallback);
textView.setCustomSelectionActionModeCallback(callback);
}
@ -112,10 +112,12 @@ public class ListSelectionManager {
private class CustomCallback implements ActionMode.Callback {
private final TextView textView;
private final ActionMode.Callback additionalCallback;
public Object identifier;
public CustomCallback(TextView textView) {
public CustomCallback(TextView textView, ActionMode.Callback additionalCallback) {
this.textView = textView;
this.additionalCallback = additionalCallback;
}
@Override
@ -123,21 +125,33 @@ public class ListSelectionManager {
selectionActionMode = mode;
selectionIdentifier = identifier;
selectionTextView = textView;
if (additionalCallback != null) {
additionalCallback.onCreateActionMode(mode, menu);
}
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
if (additionalCallback != null) {
additionalCallback.onPrepareActionMode(mode, menu);
}
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) {
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
if (additionalCallback != null) {
additionalCallback.onDestroyActionMode(mode);
}
if (selectionActionMode == mode) {
selectionActionMode = null;
selectionIdentifier = null;

View file

@ -206,9 +206,13 @@ public final class CryptoHelper {
}
public static String getAccountFingerprint(Account account) {
return getFingerprint(account.getJid().toBareJid().toString());
}
public static String getFingerprint(String value) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
return bytesToHex(md.digest(account.getJid().toBareJid().toString().getBytes("UTF-8")));
return bytesToHex(md.digest(value.getBytes("UTF-8")));
} catch (Exception e) {
return "";
}

View file

@ -19,6 +19,7 @@ import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.TreeMap;
import java.util.Map;
@ -148,26 +149,29 @@ public class DNSHelper {
for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) {
for (Record rr : rrset) {
Data d = rr.getPayload();
if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) {
final String name = rr.getName() != null ? rr.getName().toLowerCase(Locale.US) : null;
if (d instanceof SRV && NameUtil.idnEquals(qname, name)) {
SRV srv = (SRV) d;
if (!priorities.containsKey(srv.getPriority())) {
priorities.put(srv.getPriority(),new ArrayList<TlsSrv>());
}
priorities.get(srv.getPriority()).add(new TlsSrv(srv, tls));
} else if (d instanceof SRV) {
Log.d(Config.LOGTAG,"found unrecognized SRV record with name: "+name);
}
if (d instanceof A) {
A a = (A) d;
if (!ips4.containsKey(rr.getName())) {
ips4.put(rr.getName(), new ArrayList<String>());
if (!ips4.containsKey(name)) {
ips4.put(name, new ArrayList<String>());
}
ips4.get(rr.getName()).add(a.toString());
ips4.get(name).add(a.toString());
}
if (d instanceof AAAA) {
AAAA aaaa = (AAAA) d;
if (!ips6.containsKey(rr.getName())) {
ips6.put(rr.getName(), new ArrayList<String>());
if (!ips6.containsKey(name)) {
ips6.put(name, new ArrayList<String>());
}
ips6.get(rr.getName()).add("[" + aaaa.toString() + "]");
ips6.get(name).add("[" + aaaa.toString() + "]");
}
}
}
@ -177,8 +181,8 @@ public class DNSHelper {
Bundle bundle = new Bundle();
try {
client.setTimeout(Config.SOCKET_TIMEOUT * 1000);
final String qname = "_xmpp-client._tcp." + host;
final String tlsQname = "_xmpps-client._tcp." + host;
final String qname = "_xmpp-client._tcp." + host.toLowerCase(Locale.US);
final String tlsQname = "_xmpps-client._tcp." + host.toLowerCase(Locale.US);
Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host);
final Map<Integer, List<TlsSrv>> priorities = new TreeMap<>();
@ -218,27 +222,28 @@ public class DNSHelper {
}
for (final TlsSrv tlsSrv : result) {
final SRV srv = tlsSrv.srv;
if (ips6.containsKey(srv.getName())) {
values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6, tlsSrv.tls));
final String name = srv.getName() != null ? srv.getName().toLowerCase(Locale.US) : null;
if (ips6.containsKey(name)) {
values.add(createNamePortBundle(name,srv.getPort(),ips6, tlsSrv.tls));
} else {
try {
DNSMessage response = client.query(srv.getName(), TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress());
DNSMessage response = client.query(name, TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress());
for (int i = 0; i < response.getAnswers().length; ++i) {
values.add(createNamePortBundle(srv.getName(), srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls));
values.add(createNamePortBundle(name, srv.getPort(), response.getAnswers()[i].getPayload(), tlsSrv.tls));
}
} catch (SocketTimeoutException e) {
Log.d(Config.LOGTAG,"ignoring timeout exception when querying AAAA record on "+dnsServer.getHostAddress());
}
}
if (ips4.containsKey(srv.getName())) {
values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4, tlsSrv.tls));
if (ips4.containsKey(name)) {
values.add(createNamePortBundle(name,srv.getPort(),ips4, tlsSrv.tls));
} else {
DNSMessage response = client.query(srv.getName(), TYPE.A, CLASS.IN, dnsServer.getHostAddress());
DNSMessage response = client.query(name, TYPE.A, CLASS.IN, dnsServer.getHostAddress());
for(int i = 0; i < response.getAnswers().length; ++i) {
values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls));
values.add(createNamePortBundle(name,srv.getPort(),response.getAnswers()[i].getPayload(), tlsSrv.tls));
}
}
values.add(createNamePortBundle(srv.getName(), srv.getPort(), tlsSrv.tls));
values.add(createNamePortBundle(name, srv.getPort(), tlsSrv.tls));
}
bundle.putParcelableArrayList("values", values);
} catch (SocketTimeoutException e) {

View file

@ -0,0 +1,4 @@
package eu.siacs.conversations.utils;
public class FileWriterException extends Exception {
}

View file

@ -14,6 +14,9 @@
* limitations under the License.
*/
package eu.siacs.conversations.utils;
import android.content.Context;
import android.net.Uri;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
@ -484,4 +487,22 @@ public final class MimeUtils {
}
return mimeTypeToExtensionMap.get(mimeType);
}
public static String guessMimeTypeFromUri(Context context, Uri uri) {
// try the content resolver
String mimeType = context.getContentResolver().getType(uri);
// try the extension
if (mimeType == null && uri.getPath() != null) {
String path = uri.getPath();
int start = path.lastIndexOf('.') + 1;
if (start < path.length()) {
mimeType = MimeUtils.guessMimeTypeFromExtension(path.substring(start));
}
}
// sometimes this works (as with the commit content api)
if (mimeType == null) {
mimeType = uri.getQueryParameter("mimeType");
}
return mimeType;
}
}

View file

@ -0,0 +1,474 @@
/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*
* Download latest version here: https://android.googlesource.com/platform/frameworks/base.git/+/master
*
*
*/
package eu.siacs.conversations.utils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Commonly used regular expression patterns.
*/
public class Patterns {
/**
* Regular expression to match all IANA top-level domains.
* List accurate as of 2011/07/18. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
*
* @deprecated Due to the recent profileration of gTLDs, this API is
* expected to become out-of-date very quickly. Therefore it is now
* deprecated.
*/
@Deprecated
public static final String TOP_LEVEL_DOMAIN_STR =
"((aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ "|(biz|b[abdefghijmnorstvwyz])"
+ "|(cat|com|coop|c[acdfghiklmnoruvxyz])"
+ "|d[ejkmoz]"
+ "|(edu|e[cegrstu])"
+ "|f[ijkmor]"
+ "|(gov|g[abdefghilmnpqrstuwy])"
+ "|h[kmnrtu]"
+ "|(info|int|i[delmnoqrst])"
+ "|(jobs|j[emop])"
+ "|k[eghimnprwyz]"
+ "|l[abcikrstuvy]"
+ "|(mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ "|(name|net|n[acefgilopruz])"
+ "|(org|om)"
+ "|(pro|p[aefghklmnrstwy])"
+ "|qa"
+ "|r[eosuw]"
+ "|s[abcdeghijklmnortuvyz]"
+ "|(tel|travel|t[cdfghjklmnoprtvwz])"
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
+ "|(\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
+ "|y[et]"
+ "|z[amw])";
/**
* Regular expression pattern to match all IANA top-level domains.
* @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}.
*/
@Deprecated
public static final Pattern TOP_LEVEL_DOMAIN =
Pattern.compile(TOP_LEVEL_DOMAIN_STR);
/**
* Regular expression to match all IANA top-level domains for WEB_URL.
* List accurate as of 2011/07/18. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
*
* @deprecated This API is deprecated. See {@link #TOP_LEVEL_DOMAIN_STR}.
*/
@Deprecated
public static final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL =
"(?:"
+ "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ "|(?:biz|b[abdefghijmnorstvwyz])"
+ "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
+ "|d[ejkmoz]"
+ "|(?:edu|e[cegrstu])"
+ "|f[ijkmor]"
+ "|(?:gov|g[abdefghilmnpqrstuwy])"
+ "|h[kmnrtu]"
+ "|(?:info|int|i[delmnoqrst])"
+ "|(?:jobs|j[emop])"
+ "|k[eghimnprwyz]"
+ "|l[abcikrstuvy]"
+ "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ "|(?:name|net|n[acefgilopruz])"
+ "|(?:org|om)"
+ "|(?:pro|p[aefghklmnrstwy])"
+ "|qa"
+ "|r[eosuw]"
+ "|s[abcdeghijklmnortuvyz]"
+ "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
+ "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
+ "|y[et]"
+ "|z[amw]))";
/**
* Regular expression to match all IANA top-level domains.
*
* List accurate as of 2015/11/24. List taken from:
* http://data.iana.org/TLD/tlds-alpha-by-domain.txt
* This pattern is auto-generated by frameworks/ex/common/tools/make-iana-tld-pattern.py
*
* @hide
*/
static final String IANA_TOP_LEVEL_DOMAINS =
"(?:"
+ "(?:aaa|aarp|abb|abbott|abogado|academy|accenture|accountant|accountants|aco|active"
+ "|actor|ads|adult|aeg|aero|afl|agency|aig|airforce|airtel|allfinanz|alsace|amica|amsterdam"
+ "|android|apartments|app|apple|aquarelle|aramco|archi|army|arpa|arte|asia|associates"
+ "|attorney|auction|audio|auto|autos|axa|azure|a[cdefgilmoqrstuwxz])"
+ "|(?:band|bank|bar|barcelona|barclaycard|barclays|bargains|bauhaus|bayern|bbc|bbva"
+ "|bcn|beats|beer|bentley|berlin|best|bet|bharti|bible|bid|bike|bing|bingo|bio|biz|black"
+ "|blackfriday|bloomberg|blue|bms|bmw|bnl|bnpparibas|boats|bom|bond|boo|boots|boutique"
+ "|bradesco|bridgestone|broadway|broker|brother|brussels|budapest|build|builders|business"
+ "|buzz|bzh|b[abdefghijmnorstvwyz])"
+ "|(?:cab|cafe|cal|camera|camp|cancerresearch|canon|capetown|capital|car|caravan|cards"
+ "|care|career|careers|cars|cartier|casa|cash|casino|cat|catering|cba|cbn|ceb|center|ceo"
+ "|cern|cfa|cfd|chanel|channel|chat|cheap|chloe|christmas|chrome|church|cipriani|cisco"
+ "|citic|city|cityeats|claims|cleaning|click|clinic|clothing|cloud|club|clubmed|coach"
+ "|codes|coffee|college|cologne|com|commbank|community|company|computer|comsec|condos"
+ "|construction|consulting|contractors|cooking|cool|coop|corsica|country|coupons|courses"
+ "|credit|creditcard|creditunion|cricket|crown|crs|cruises|csc|cuisinella|cymru|cyou|c[acdfghiklmnoruvwxyz])"
+ "|(?:dabur|dad|dance|date|dating|datsun|day|dclk|deals|degree|delivery|dell|delta"
+ "|democrat|dental|dentist|desi|design|dev|diamonds|diet|digital|direct|directory|discount"
+ "|dnp|docs|dog|doha|domains|doosan|download|drive|durban|dvag|d[ejkmoz])"
+ "|(?:earth|eat|edu|education|email|emerck|energy|engineer|engineering|enterprises"
+ "|epson|equipment|erni|esq|estate|eurovision|eus|events|everbank|exchange|expert|exposed"
+ "|express|e[cegrstu])"
+ "|(?:fage|fail|fairwinds|faith|family|fan|fans|farm|fashion|feedback|ferrero|film"
+ "|final|finance|financial|firmdale|fish|fishing|fit|fitness|flights|florist|flowers|flsmidth"
+ "|fly|foo|football|forex|forsale|forum|foundation|frl|frogans|fund|furniture|futbol|fyi"
+ "|f[ijkmor])"
+ "|(?:gal|gallery|game|garden|gbiz|gdn|gea|gent|genting|ggee|gift|gifts|gives|giving"
+ "|glass|gle|global|globo|gmail|gmo|gmx|gold|goldpoint|golf|goo|goog|google|gop|gov|grainger"
+ "|graphics|gratis|green|gripe|group|gucci|guge|guide|guitars|guru|g[abdefghilmnpqrstuwy])"
+ "|(?:hamburg|hangout|haus|healthcare|help|here|hermes|hiphop|hitachi|hiv|hockey|holdings"
+ "|holiday|homedepot|homes|honda|horse|host|hosting|hoteles|hotmail|house|how|hsbc|hyundai"
+ "|h[kmnrtu])"
+ "|(?:ibm|icbc|ice|icu|ifm|iinet|immo|immobilien|industries|infiniti|info|ing|ink|institute"
+ "|insure|int|international|investments|ipiranga|irish|ist|istanbul|itau|iwc|i[delmnoqrst])"
+ "|(?:jaguar|java|jcb|jetzt|jewelry|jlc|jll|jobs|joburg|jprs|juegos|j[emop])"
+ "|(?:kaufen|kddi|kia|kim|kinder|kitchen|kiwi|koeln|komatsu|krd|kred|kyoto|k[eghimnprwyz])"
+ "|(?:lacaixa|lancaster|land|landrover|lasalle|lat|latrobe|law|lawyer|lds|lease|leclerc"
+ "|legal|lexus|lgbt|liaison|lidl|life|lifestyle|lighting|limited|limo|linde|link|live"
+ "|lixil|loan|loans|lol|london|lotte|lotto|love|ltd|ltda|lupin|luxe|luxury|l[abcikrstuvy])"
+ "|(?:madrid|maif|maison|man|management|mango|market|marketing|markets|marriott|mba"
+ "|media|meet|melbourne|meme|memorial|men|menu|meo|miami|microsoft|mil|mini|mma|mobi|moda"
+ "|moe|moi|mom|monash|money|montblanc|mormon|mortgage|moscow|motorcycles|mov|movie|movistar"
+ "|mtn|mtpc|mtr|museum|mutuelle|m[acdeghklmnopqrstuvwxyz])"
+ "|(?:nadex|nagoya|name|navy|nec|net|netbank|network|neustar|new|news|nexus|ngo|nhk"
+ "|nico|ninja|nissan|nokia|nra|nrw|ntt|nyc|n[acefgilopruz])"
+ "|(?:obi|office|okinawa|omega|one|ong|onl|online|ooo|oracle|orange|org|organic|osaka"
+ "|otsuka|ovh|om)"
+ "|(?:page|panerai|paris|partners|parts|party|pet|pharmacy|philips|photo|photography"
+ "|photos|physio|piaget|pics|pictet|pictures|ping|pink|pizza|place|play|playstation|plumbing"
+ "|plus|pohl|poker|porn|post|praxi|press|pro|prod|productions|prof|properties|property"
+ "|protection|pub|p[aefghklmnrstwy])"
+ "|(?:qpon|quebec|qa)"
+ "|(?:racing|realtor|realty|recipes|red|redstone|rehab|reise|reisen|reit|ren|rent|rentals"
+ "|repair|report|republican|rest|restaurant|review|reviews|rich|ricoh|rio|rip|rocher|rocks"
+ "|rodeo|rsvp|ruhr|run|rwe|ryukyu|r[eosuw])"
+ "|(?:saarland|sakura|sale|samsung|sandvik|sandvikcoromant|sanofi|sap|sapo|sarl|saxo"
+ "|sbs|sca|scb|schmidt|scholarships|school|schule|schwarz|science|scor|scot|seat|security"
+ "|seek|sener|services|seven|sew|sex|sexy|shiksha|shoes|show|shriram|singles|site|ski"
+ "|sky|skype|sncf|soccer|social|software|sohu|solar|solutions|sony|soy|space|spiegel|spreadbetting"
+ "|srl|stada|starhub|statoil|stc|stcgroup|stockholm|studio|study|style|sucks|supplies"
+ "|supply|support|surf|surgery|suzuki|swatch|swiss|sydney|systems|s[abcdeghijklmnortuvxyz])"
+ "|(?:tab|taipei|tatamotors|tatar|tattoo|tax|taxi|team|tech|technology|tel|telefonica"
+ "|temasek|tennis|thd|theater|theatre|tickets|tienda|tips|tires|tirol|today|tokyo|tools"
+ "|top|toray|toshiba|tours|town|toyota|toys|trade|trading|training|travel|trust|tui|t[cdfghjklmnortvwz])"
+ "|(?:ubs|university|uno|uol|u[agksyz])"
+ "|(?:vacations|vana|vegas|ventures|versicherung|vet|viajes|video|villas|vin|virgin"
+ "|vision|vista|vistaprint|viva|vlaanderen|vodka|vote|voting|voto|voyage|v[aceginu])"
+ "|(?:wales|walter|wang|watch|webcam|website|wed|wedding|weir|whoswho|wien|wiki|williamhill"
+ "|win|windows|wine|wme|work|works|world|wtc|wtf|w[fs])"
+ "|(?:\u03b5\u03bb|\u0431\u0435\u043b|\u0434\u0435\u0442\u0438|\u043a\u043e\u043c|\u043c\u043a\u0434"
+ "|\u043c\u043e\u043d|\u043c\u043e\u0441\u043a\u0432\u0430|\u043e\u043d\u043b\u0430\u0439\u043d"
+ "|\u043e\u0440\u0433|\u0440\u0443\u0441|\u0440\u0444|\u0441\u0430\u0439\u0442|\u0441\u0440\u0431"
+ "|\u0443\u043a\u0440|\u049b\u0430\u0437|\u0570\u0561\u0575|\u05e7\u05d5\u05dd|\u0627\u0631\u0627\u0645\u0643\u0648"
+ "|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629"
+ "|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0627\u06cc\u0631\u0627\u0646"
+ "|\u0628\u0627\u0632\u0627\u0631|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633"
+ "|\u0633\u0648\u062f\u0627\u0646|\u0633\u0648\u0631\u064a\u0629|\u0634\u0628\u0643\u0629"
+ "|\u0639\u0631\u0627\u0642|\u0639\u0645\u0627\u0646|\u0641\u0644\u0633\u0637\u064a\u0646"
+ "|\u0642\u0637\u0631|\u0643\u0648\u0645|\u0645\u0635\u0631|\u0645\u0644\u064a\u0633\u064a\u0627"
+ "|\u0645\u0648\u0642\u0639|\u0915\u0949\u092e|\u0928\u0947\u091f|\u092d\u093e\u0930\u0924"
+ "|\u0938\u0902\u0917\u0920\u0928|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4"
+ "|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd"
+ "|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e04\u0e2d\u0e21|\u0e44\u0e17\u0e22"
+ "|\u10d2\u10d4|\u307f\u3093\u306a|\u30b0\u30fc\u30b0\u30eb|\u30b3\u30e0|\u4e16\u754c"
+ "|\u4e2d\u4fe1|\u4e2d\u56fd|\u4e2d\u570b|\u4e2d\u6587\u7f51|\u4f01\u4e1a|\u4f5b\u5c71"
+ "|\u4fe1\u606f|\u5065\u5eb7|\u516b\u5366|\u516c\u53f8|\u516c\u76ca|\u53f0\u6e7e|\u53f0\u7063"
+ "|\u5546\u57ce|\u5546\u5e97|\u5546\u6807|\u5728\u7ebf|\u5927\u62ff|\u5a31\u4e50|\u5de5\u884c"
+ "|\u5e7f\u4e1c|\u6148\u5584|\u6211\u7231\u4f60|\u624b\u673a|\u653f\u52a1|\u653f\u5e9c"
+ "|\u65b0\u52a0\u5761|\u65b0\u95fb|\u65f6\u5c1a|\u673a\u6784|\u6de1\u9a6c\u9521|\u6e38\u620f"
+ "|\u70b9\u770b|\u79fb\u52a8|\u7ec4\u7ec7\u673a\u6784|\u7f51\u5740|\u7f51\u5e97|\u7f51\u7edc"
+ "|\u8c37\u6b4c|\u96c6\u56e2|\u98de\u5229\u6d66|\u9910\u5385|\u9999\u6e2f|\ub2f7\ub137"
+ "|\ub2f7\ucef4|\uc0bc\uc131|\ud55c\uad6d|xbox"
+ "|xerox|xin|xn\\-\\-11b4c3d|xn\\-\\-1qqw23a|xn\\-\\-30rr7y|xn\\-\\-3bst00m|xn\\-\\-3ds443g"
+ "|xn\\-\\-3e0b707e|xn\\-\\-3pxu8k|xn\\-\\-42c2d9a|xn\\-\\-45brj9c|xn\\-\\-45q11c|xn\\-\\-4gbrim"
+ "|xn\\-\\-55qw42g|xn\\-\\-55qx5d|xn\\-\\-6frz82g|xn\\-\\-6qq986b3xl|xn\\-\\-80adxhks"
+ "|xn\\-\\-80ao21a|xn\\-\\-80asehdb|xn\\-\\-80aswg|xn\\-\\-90a3ac|xn\\-\\-90ais|xn\\-\\-9dbq2a"
+ "|xn\\-\\-9et52u|xn\\-\\-b4w605ferd|xn\\-\\-c1avg|xn\\-\\-c2br7g|xn\\-\\-cg4bki|xn\\-\\-clchc0ea0b2g2a9gcd"
+ "|xn\\-\\-czr694b|xn\\-\\-czrs0t|xn\\-\\-czru2d|xn\\-\\-d1acj3b|xn\\-\\-d1alf|xn\\-\\-efvy88h"
+ "|xn\\-\\-estv75g|xn\\-\\-fhbei|xn\\-\\-fiq228c5hs|xn\\-\\-fiq64b|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s"
+ "|xn\\-\\-fjq720a|xn\\-\\-flw351e|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-gecrj9c"
+ "|xn\\-\\-h2brj9c|xn\\-\\-hxt814e|xn\\-\\-i1b6b1a6a2e|xn\\-\\-imr513n|xn\\-\\-io0a7i"
+ "|xn\\-\\-j1aef|xn\\-\\-j1amh|xn\\-\\-j6w193g|xn\\-\\-kcrx77d1x4a|xn\\-\\-kprw13d|xn\\-\\-kpry57d"
+ "|xn\\-\\-kput3i|xn\\-\\-l1acc|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgb9awbf|xn\\-\\-mgba3a3ejt"
+ "|xn\\-\\-mgba3a4f16a|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbab2bd|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e"
+ "|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-mgbpl2fh|xn\\-\\-mgbtx2b|xn\\-\\-mgbx4cd0ab"
+ "|xn\\-\\-mk1bu44c|xn\\-\\-mxtq1m|xn\\-\\-ngbc5azd|xn\\-\\-node|xn\\-\\-nqv7f|xn\\-\\-nqv7fs00ema"
+ "|xn\\-\\-nyqy26a|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1acf|xn\\-\\-p1ai|xn\\-\\-pgbs0dh"
+ "|xn\\-\\-pssy2u|xn\\-\\-q9jyb4c|xn\\-\\-qcka1pmc|xn\\-\\-qxam|xn\\-\\-rhqv96g|xn\\-\\-s9brj9c"
+ "|xn\\-\\-ses554g|xn\\-\\-t60b56a|xn\\-\\-tckwe|xn\\-\\-unup4y|xn\\-\\-vermgensberater\\-ctb"
+ "|xn\\-\\-vermgensberatung\\-pwb|xn\\-\\-vhquv|xn\\-\\-vuq861b|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a"
+ "|xn\\-\\-xhq521b|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-y9a3aq|xn\\-\\-yfro4i67o"
+ "|xn\\-\\-ygbi2ammx|xn\\-\\-zfr164b|xperia|xxx|xyz)"
+ "|(?:yachts|yamaxun|yandex|yodobashi|yoga|yokohama|youtube|y[et])"
+ "|(?:zara|zip|zone|zuerich|z[amw]))";
/**
* Kept for backward compatibility reasons.
*
* @deprecated Deprecated since it does not include all IRI characters defined in RFC 3987
*/
@Deprecated
public static final String GOOD_IRI_CHAR =
"a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";
public static final Pattern IP_ADDRESS
= Pattern.compile(
"((25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(25[0-5]|2[0-4]"
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1]"
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}"
+ "|[1-9][0-9]|[0-9]))");
/**
* Valid UCS characters defined in RFC 3987. Excludes space characters.
*/
private static final String UCS_CHAR = "[" +
"\u00A0-\uD7FF" +
"\uF900-\uFDCF" +
"\uFDF0-\uFFEF" +
"\uD800\uDC00-\uD83F\uDFFD" +
"\uD840\uDC00-\uD87F\uDFFD" +
"\uD880\uDC00-\uD8BF\uDFFD" +
"\uD8C0\uDC00-\uD8FF\uDFFD" +
"\uD900\uDC00-\uD93F\uDFFD" +
"\uD940\uDC00-\uD97F\uDFFD" +
"\uD980\uDC00-\uD9BF\uDFFD" +
"\uD9C0\uDC00-\uD9FF\uDFFD" +
"\uDA00\uDC00-\uDA3F\uDFFD" +
"\uDA40\uDC00-\uDA7F\uDFFD" +
"\uDA80\uDC00-\uDABF\uDFFD" +
"\uDAC0\uDC00-\uDAFF\uDFFD" +
"\uDB00\uDC00-\uDB3F\uDFFD" +
"\uDB44\uDC00-\uDB7F\uDFFD" +
"&&[^\u00A0[\u2000-\u200A]\u2028\u2029\u202F\u3000]]";
/**
* Valid characters for IRI label defined in RFC 3987.
*/
private static final String LABEL_CHAR = "a-zA-Z0-9" + UCS_CHAR;
/**
* Valid characters for IRI TLD defined in RFC 3987.
*/
private static final String TLD_CHAR = "a-zA-Z" + UCS_CHAR;
/**
* RFC 1035 Section 2.3.4 limits the labels to a maximum 63 octets.
*/
private static final String IRI_LABEL =
"[" + LABEL_CHAR + "](?:[" + LABEL_CHAR + "\\-]{0,61}[" + LABEL_CHAR + "]){0,1}";
/**
* RFC 3492 references RFC 1034 and limits Punycode algorithm output to 63 characters.
*/
private static final String PUNYCODE_TLD = "xn\\-\\-[\\w\\-]{0,58}\\w";
private static final String TLD = "(" + PUNYCODE_TLD + "|" + "[" + TLD_CHAR + "]{2,63}" +")";
private static final String HOST_NAME = "(" + IRI_LABEL + "\\.)+" + TLD;
public static final Pattern DOMAIN_NAME
= Pattern.compile("(" + HOST_NAME + "|" + IP_ADDRESS + ")");
private static final String PROTOCOL = "(?i:http|https|rtsp):\\/\\/";
/* A word boundary or end of input. This is to stop foo.sure from matching as foo.su */
private static final String WORD_BOUNDARY = "(?:\\b|$|^)";
private static final String USER_INFO = "(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@";
private static final String PORT_NUMBER = "\\:\\d{1,5}";
private static final String PATH_AND_QUERY = "\\/(?:(?:[" + LABEL_CHAR
+ "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus optional query params
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*";
/**
* Regular expression pattern to match most part of RFC 3987
* Internationalized URLs, aka IRIs.
*/
public static final Pattern WEB_URL = Pattern.compile("("
+ "("
+ "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")?"
+ "(?:" + DOMAIN_NAME + ")"
+ "(?:" + PORT_NUMBER + ")?"
+ ")"
+ "(" + PATH_AND_QUERY + ")?"
+ WORD_BOUNDARY
+ ")");
/**
* Regular expression that matches known TLDs and punycode TLDs
*/
private static final String STRICT_TLD = "(?:" +
IANA_TOP_LEVEL_DOMAINS + "|" + PUNYCODE_TLD + ")";
/**
* Regular expression that matches host names using {@link #STRICT_TLD}
*/
private static final String STRICT_HOST_NAME = "(?:(?:" + IRI_LABEL + "\\.)+"
+ STRICT_TLD + ")";
/**
* Regular expression that matches domain names using either {@link #STRICT_HOST_NAME} or
* {@link #IP_ADDRESS}
*/
private static final Pattern STRICT_DOMAIN_NAME
= Pattern.compile("(?:" + STRICT_HOST_NAME + "|" + IP_ADDRESS + ")");
/**
* Regular expression that matches domain names without a TLD
*/
private static final String RELAXED_DOMAIN_NAME =
"(?:" + "(?:" + IRI_LABEL + "(?:\\.(?=\\S))" +"?)+" + "|" + IP_ADDRESS + ")";
/**
* Regular expression to match strings that do not start with a supported protocol. The TLDs
* are expected to be one of the known TLDs.
*/
private static final String WEB_URL_WITHOUT_PROTOCOL = "("
+ WORD_BOUNDARY
+ "(?<!:\\/\\/)"
+ "("
+ "(?:" + STRICT_DOMAIN_NAME + ")"
+ "(?:" + PORT_NUMBER + ")?"
+ ")"
+ "(?:" + PATH_AND_QUERY + ")?"
+ WORD_BOUNDARY
+ ")";
/**
* Regular expression to match strings that start with a supported protocol. Rules for domain
* names and TLDs are more relaxed. TLDs are optional.
*/
private static final String WEB_URL_WITH_PROTOCOL = "("
+ WORD_BOUNDARY
+ "(?:"
+ "(?:" + PROTOCOL + "(?:" + USER_INFO + ")?" + ")"
+ "(?:" + RELAXED_DOMAIN_NAME + ")?"
+ "(?:" + PORT_NUMBER + ")?"
+ ")"
+ "(?:" + PATH_AND_QUERY + ")?"
+ WORD_BOUNDARY
+ ")";
/**
* Regular expression pattern to match IRIs. If a string starts with http(s):// the expression
* tries to match the URL structure with a relaxed rule for TLDs. If the string does not start
* with http(s):// the TLDs are expected to be one of the known TLDs.
*
* @hide
*/
public static final Pattern AUTOLINK_WEB_URL = Pattern.compile(
"(" + WEB_URL_WITH_PROTOCOL + "|" + WEB_URL_WITHOUT_PROTOCOL + ")");
/**
* Regular expression for valid email characters. Does not include some of the valid characters
* defined in RFC5321: #&~!^`{}/=$*?|
*/
private static final String EMAIL_CHAR = LABEL_CHAR + "\\+\\-_%'";
/**
* Regular expression for local part of an email address. RFC5321 section 4.5.3.1.1 limits
* the local part to be at most 64 octets.
*/
private static final String EMAIL_ADDRESS_LOCAL_PART =
"[" + EMAIL_CHAR + "]" + "(?:[" + EMAIL_CHAR + "\\.]{1,62}[" + EMAIL_CHAR + "])?";
/**
* Regular expression for the domain part of an email address. RFC5321 section 4.5.3.1.2 limits
* the domain to be at most 255 octets.
*/
private static final String EMAIL_ADDRESS_DOMAIN =
"(?=.{1,255}(?:\\s|$|^))" + HOST_NAME;
/**
* Regular expression pattern to match email addresses. It excludes double quoted local parts
* and the special characters #&~!^`{}/=$*?| that are included in RFC5321.
* @hide
*/
public static final Pattern AUTOLINK_EMAIL_ADDRESS = Pattern.compile("(" + WORD_BOUNDARY +
"(?:" + EMAIL_ADDRESS_LOCAL_PART + "@" + EMAIL_ADDRESS_DOMAIN + ")" +
WORD_BOUNDARY + ")"
);
public static final Pattern EMAIL_ADDRESS
= Pattern.compile(
"[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
"\\@" +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
"(" +
"\\." +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
")+"
);
/**
* This pattern is intended for searching for things that look like they
* might be phone numbers in arbitrary text, not for validating whether
* something is in fact a phone number. It will miss many things that
* are legitimate phone numbers.
*
* <p> The pattern matches the following:
* <ul>
* <li>Optionally, a + sign followed immediately by one or more digits. Spaces, dots, or dashes
* may follow.
* <li>Optionally, sets of digits in parentheses, separated by spaces, dots, or dashes.
* <li>A string starting and ending with a digit, containing digits, spaces, dots, and/or dashes.
* </ul>
*/
public static final Pattern PHONE
= Pattern.compile( // sdd = space, dot, or dash
"(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>*
+ "(\\([0-9]+\\)[\\- \\.]*)?" // (<digits>)<sdd>*
+ "([0-9][0-9\\- \\.]+[0-9])"); // <digit><digit|sdd>+<digit>
/**
* Convenience method to take all of the non-null matching groups in a
* regex Matcher and return them as a concatenated string.
*
* @param matcher The Matcher object from which grouped text will
* be extracted
*
* @return A String comprising all of the non-null matched
* groups concatenated together
*/
public static final String concatGroups(Matcher matcher) {
StringBuilder b = new StringBuilder();
final int numGroups = matcher.groupCount();
for (int i = 1; i <= numGroups; i++) {
String s = matcher.group(i);
if (s != null) {
b.append(s);
}
}
return b.toString();
}
/**
* Convenience method to return only the digits and plus signs
* in the matching string.
*
* @param matcher The Matcher object from which digits and plus will
* be extracted
*
* @return A String comprising all of the digits and plus in
* the match
*/
public static final String digitsAndPlusOnly(Matcher matcher) {
StringBuilder buffer = new StringBuilder();
String matchingRegion = matcher.group();
for (int i = 0, size = matchingRegion.length(); i < size; i++) {
char character = matchingRegion.charAt(i);
if (character == '+' || Character.isDigit(character)) {
buffer.append(character);
}
}
return buffer.toString();
}
/**
* Do not create this static utility class.
*/
private Patterns() {}
}

View file

@ -0,0 +1,69 @@
package eu.siacs.conversations.utils;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
public class TLSSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory internalSSLSocketFactory;
public TLSSocketFactory(X509TrustManager[] trustManager, SecureRandom random) throws KeyManagementException, NoSuchAlgorithmException {
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, trustManager, random);
this.internalSSLSocketFactory = context.getSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return CryptoHelper.getOrderedCipherSuites(internalSSLSocketFactory.getDefaultCipherSuites());
}
@Override
public String[] getSupportedCipherSuites() {
return internalSSLSocketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(s, host, port, autoClose));
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port, localHost, localPort));
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(host, port));
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return enableTLSOnSocket(internalSSLSocketFactory.createSocket(address, port, localAddress, localPort));
}
private static Socket enableTLSOnSocket(Socket socket) {
if(socket != null && (socket instanceof SSLSocket)) {
try {
SSLSocketHelper.setSecurity((SSLSocket) socket);
} catch (NoSuchAlgorithmException e) {
//ignoring
}
}
return socket;
}
}

View file

@ -9,6 +9,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
@ -29,9 +30,9 @@ public class UIHelper {
private static String HEAVY_BLACK_HEART_SUIT = "\u2764";
private static String WHITE_HEART_SUIT = "\u2661";
public static final ArrayList<String> HEARTS = new ArrayList<>(Arrays.asList(BLACK_HEART_SUIT,HEAVY_BLACK_HEART_SUIT,WHITE_HEART_SUIT));
public static final List<String> HEARTS = Arrays.asList(BLACK_HEART_SUIT,HEAVY_BLACK_HEART_SUIT,WHITE_HEART_SUIT);
private static final ArrayList<String> LOCATION_QUESTIONS = new ArrayList<>(Arrays.asList(
private static final List<String> LOCATION_QUESTIONS = Arrays.asList(
"where are you", //en
"where are you now", //en
"where are you right now", //en
@ -49,7 +50,9 @@ public class UIHelper {
"wo seid ihr gerade", //de
"dónde estás", //es
"donde estas" //es
));
);
private static final List<Character> PUNCTIONATION = Arrays.asList('.',',','?','!',';',':');
private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
@ -180,10 +183,7 @@ public class UIHelper {
return new Pair<>(getFileDescriptionString(context,message),true);
}
} else {
String body = message.getBody();
if (body.length() > 256) {
body = body.substring(0,256);
}
final String body = message.getBody();
if (body.startsWith(Message.ME_COMMAND)) {
return new Pair<>(body.replaceAll("^" + Message.ME_COMMAND,
UIHelper.getMessageDisplayName(message) + " "), false);
@ -196,12 +196,51 @@ public class UIHelper {
} else if (message.treatAsDownloadable() == Message.Decision.MUST) {
return new Pair<>(context.getString(R.string.x_file_offered_for_download,
getFileDescriptionString(context,message)),true);
} else{
return new Pair<>(body.trim(), false);
} else {
String[] lines = body.split("\n");
StringBuilder builder = new StringBuilder();
for(String l : lines) {
if (l.length() > 0) {
char first = l.charAt(0);
if ((first != '>' || isPositionFollowedByNumber(l,0)) && first != '\u00bb') {
String line = l.trim();
if (line.isEmpty()) {
continue;
}
char last = line.charAt(line.length()-1);
if (builder.length() != 0) {
builder.append(' ');
}
builder.append(line);
if (!PUNCTIONATION.contains(last)) {
break;
}
}
}
}
if (builder.length() == 0) {
builder.append(body.trim());
}
return new Pair<>(builder.length() > 256 ? builder.substring(0,256) : builder.toString(), false);
}
}
}
public static boolean isPositionFollowedByNumber(CharSequence body, int pos) {
boolean previousWasNumber = false;
for (int i = pos +1; i < body.length(); i++) {
char c = body.charAt(i);
if (Character.isDigit(body.charAt(i))) {
previousWasNumber = true;
} else if (previousWasNumber && (c == '.' || c == ',')) {
previousWasNumber = false;
} else {
return Character.isWhitespace(c) && previousWasNumber;
}
}
return previousWasNumber;
}
public static String getFileDescriptionString(final Context context, final Message message) {
if (message.getType() == Message.TYPE_IMAGE) {
return context.getString(R.string.image);

View file

@ -6,4 +6,5 @@ public final class Xmlns {
public static final String REGISTER = "jabber:iq:register";
public static final String BYTE_STREAMS = "http://jabber.org/protocol/bytestreams";
public static final String HTTP_UPLOAD = "urn:xmpp:http:upload";
public static final String STANZA_IDS = "urn:xmpp:sid:0";
}

View file

@ -4,7 +4,9 @@ import android.net.Uri;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@ -13,7 +15,12 @@ public class XmppUri {
protected String jid;
protected boolean muc;
protected String fingerprint;
protected List<Fingerprint> fingerprints = new ArrayList<>();
private String body;
protected boolean safeSource = true;
public static final String OMEMO_URI_PARAM = "omemo-sid-";
public static final String OTR_URI_PARAM = "otr-fingerprint";
public XmppUri(String uri) {
try {
@ -31,6 +38,15 @@ public class XmppUri {
parse(uri);
}
public XmppUri(Uri uri, boolean safeSource) {
this.safeSource = safeSource;
parse(uri);
}
public boolean isSafeSource() {
return safeSource;
}
protected void parse(Uri uri) {
String scheme = uri.getScheme();
String host = uri.getHost();
@ -48,15 +64,17 @@ public class XmppUri {
jid = segments.get(1) + "@" + segments.get(2);
}
muc = segments.size() > 1 && "j".equalsIgnoreCase(segments.get(0));
fingerprints = parseFingerprints(uri.getQuery(),'&');
} else if ("xmpp".equalsIgnoreCase(scheme)) {
// sample: xmpp:foo@bar.com
muc = "join".equalsIgnoreCase(uri.getQuery());
muc = isMuc(uri.getQuery());
if (uri.getAuthority() != null) {
jid = uri.getAuthority();
} else {
jid = uri.getSchemeSpecificPart().split("\\?")[0];
}
fingerprint = parseFingerprint(uri.getQuery());
this.fingerprints = parseFingerprints(uri.getQuery());
this.body = parseBody(uri.getQuery());
} else if ("imto".equalsIgnoreCase(scheme)) {
// sample: imto://xmpp/foo@bar.com
try {
@ -73,18 +91,56 @@ public class XmppUri {
}
}
protected String parseFingerprint(String query) {
if (query == null) {
return null;
} else {
final String NEEDLE = "otr-fingerprint=";
int index = query.indexOf(NEEDLE);
if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) {
return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40);
} else {
return null;
protected List<Fingerprint> parseFingerprints(String query) {
return parseFingerprints(query,';');
}
protected List<Fingerprint> parseFingerprints(String query, char seperator) {
List<Fingerprint> fingerprints = new ArrayList<>();
String[] pairs = query == null ? new String[0] : query.split(String.valueOf(seperator));
for(String pair : pairs) {
String[] parts = pair.split("=",2);
if (parts.length == 2) {
String key = parts[0].toLowerCase(Locale.US);
String value = parts[1].toLowerCase(Locale.US);
if (OTR_URI_PARAM.equals(key)) {
fingerprints.add(new Fingerprint(FingerprintType.OTR,value));
}
if (key.startsWith(OMEMO_URI_PARAM)) {
try {
int id = Integer.parseInt(key.substring(OMEMO_URI_PARAM.length()));
fingerprints.add(new Fingerprint(FingerprintType.OMEMO,value,id));
} catch (Exception e) {
//ignoring invalid device id
}
}
}
}
return fingerprints;
}
protected String parseBody(String query) {
for(String pair : query == null ? new String[0] : query.split(";")) {
final String[] parts = pair.split("=",2);
if (parts.length == 2 && "body".equals(parts[0].toLowerCase(Locale.US))) {
try {
return URLDecoder.decode(parts[1],"UTF-8");
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
return null;
}
protected boolean isMuc(String query) {
for(String pair : query == null ? new String[0] : query.split(";")) {
final String[] parts = pair.split("=",2);
if (parts.length == 1 && "join".equals(parts[0])) {
return true;
}
}
return false;
}
public Jid getJid() {
@ -95,7 +151,60 @@ public class XmppUri {
}
}
public String getFingerprint() {
return this.fingerprint;
public String getBody() {
return body;
}
public List<Fingerprint> getFingerprints() {
return this.fingerprints;
}
public boolean hasFingerprints() {
return fingerprints.size() > 0;
}
public enum FingerprintType {
OMEMO,
OTR
}
public static String getFingerprintUri(String base, List<XmppUri.Fingerprint> fingerprints, char seperator) {
StringBuilder builder = new StringBuilder(base);
builder.append('?');
for(int i = 0; i < fingerprints.size(); ++i) {
XmppUri.FingerprintType type = fingerprints.get(i).type;
if (type == XmppUri.FingerprintType.OMEMO) {
builder.append(XmppUri.OMEMO_URI_PARAM);
builder.append(fingerprints.get(i).deviceId);
} else if (type == XmppUri.FingerprintType.OTR) {
builder.append(XmppUri.OTR_URI_PARAM);
}
builder.append('=');
builder.append(fingerprints.get(i).fingerprint);
if (i != fingerprints.size() -1) {
builder.append(seperator);
}
}
return builder.toString();
}
public static class Fingerprint {
public final FingerprintType type;
public final String fingerprint;
public final int deviceId;
public Fingerprint(FingerprintType type, String fingerprint) {
this(type, fingerprint, 0);
}
public Fingerprint(FingerprintType type, String fingerprint, int deviceId) {
this.type = type;
this.fingerprint = fingerprint;
this.deviceId = deviceId;
}
@Override
public String toString() {
return type.toString()+": "+fingerprint+(deviceId != 0 ? " "+String.valueOf(deviceId) : "");
}
}
}

View file

@ -99,6 +99,13 @@ public class TagWriter {
public void forceClose() {
finish();
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
//ignoring
}
}
outputStream = null;
}
}

View file

@ -82,7 +82,7 @@ public class XmlReader {
}
} catch (Throwable throwable) {
throw new IOException("xml parser mishandled "+throwable.getClass().getName(), throwable);
throw new IOException("xml parser mishandled "+throwable.getClass().getSimpleName()+"("+throwable.getMessage()+")", throwable);
} finally {
if (wakeLock.isHeld()) {
try {

View file

@ -13,8 +13,6 @@ import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParserException;
import java.io.ByteArrayInputStream;
@ -26,8 +24,8 @@ import java.net.IDN;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
@ -61,6 +59,7 @@ import eu.siacs.conversations.crypto.sasl.External;
import eu.siacs.conversations.crypto.sasl.Plain;
import eu.siacs.conversations.crypto.sasl.SaslMechanism;
import eu.siacs.conversations.crypto.sasl.ScramSha1;
import eu.siacs.conversations.crypto.sasl.ScramSha256;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
@ -178,8 +177,6 @@ public class XmppConnection implements Runnable {
}
}
private Identity mServerIdentity = Identity.UNKNOWN;
public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
@ -218,7 +215,10 @@ public class XmppConnection implements Runnable {
mXmppConnectionService = service;
}
protected void changeStatus(final Account.State nextStatus) {
protected synchronized void changeStatus(final Account.State nextStatus) {
if (Thread.currentThread().isInterrupted()) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not changing status to "+nextStatus+" because thread was interrupted");
}
if (account.getStatus() != nextStatus) {
if ((nextStatus == Account.State.OFFLINE)
&& (account.getStatus() != Account.State.CONNECTING)
@ -250,18 +250,8 @@ public class XmppConnection implements Runnable {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting");
features.encryptionEnabled = false;
this.attempt++;
switch (account.getJid().getDomainpart()) {
case "chat.facebook.com":
mServerIdentity = Identity.FACEBOOK;
break;
case "nimbuzz.com":
mServerIdentity = Identity.NIMBUZZ;
break;
default:
mServerIdentity = Identity.UNKNOWN;
break;
}
try {
Socket localSocket;
shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER);
tagReader = new XmlReader(wakeLock);
tagWriter = new TagWriter();
@ -276,8 +266,15 @@ public class XmppConnection implements Runnable {
destination = account.getHostname();
}
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": connect to " + destination + " via Tor");
socket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
startXmpp();
localSocket = SocksSocketFactory.createSocketOverTor(destination, account.getPort());
try {
startXmpp(localSocket);
} catch (InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
return;
} catch (Exception e) {
throw new IOException(e.getMessage());
}
} else if (extended && account.getHostname() != null && !account.getHostname().isEmpty()) {
InetSocketAddress address = new InetSocketAddress(account.getHostname(), account.getPort());
@ -288,33 +285,47 @@ public class XmppConnection implements Runnable {
if (features.encryptionEnabled) {
try {
final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
socket = tlsFactoryVerifier.factory.createSocket();
socket.connect(address, Config.SOCKET_TIMEOUT * 1000);
final SSLSession session = ((SSLSocket) socket).getSession();
localSocket = tlsFactoryVerifier.factory.createSocket();
localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000);
final SSLSession session = ((SSLSocket) localSocket).getSession();
if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), session)) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
throw new SecurityException();
}
} catch (KeyManagementException e) {
features.encryptionEnabled = false;
socket = new Socket();
localSocket = new Socket();
}
} else {
socket = new Socket();
socket.connect(address, Config.SOCKET_TIMEOUT * 1000);
localSocket = new Socket();
localSocket.connect(address, Config.SOCKET_TIMEOUT * 1000);
}
} catch (IOException e) {
throw new UnknownHostException();
}
startXmpp();
} else if (DNSHelper.isIp(account.getServer().toString())) {
socket = new Socket();
try {
socket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000);
startXmpp(localSocket);
} catch (InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
return;
} catch (Exception e) {
throw new IOException(e.getMessage());
}
} else if (DNSHelper.isIp(account.getServer().toString())) {
localSocket = new Socket();
try {
localSocket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000);
} catch (IOException e) {
throw new UnknownHostException();
}
startXmpp();
try {
startXmpp(localSocket);
} catch (InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
return;
} catch (Exception e) {
throw new IOException(e.getMessage());
}
} else {
final Bundle result = DNSHelper.getSRVRecord(account.getServer(), mXmppConnectionService);
final ArrayList<Parcelable> values = result.getParcelableArrayList("values");
@ -350,32 +361,37 @@ public class XmppConnection implements Runnable {
}
if (!features.encryptionEnabled) {
socket = new Socket();
socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
localSocket = new Socket();
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
} else {
final TlsFactoryVerifier tlsFactoryVerifier = getTlsFactoryVerifier();
socket = tlsFactoryVerifier.factory.createSocket();
localSocket = tlsFactoryVerifier.factory.createSocket();
if (socket == null) {
if (localSocket == null) {
throw new IOException("could not initialize ssl socket");
}
SSLSocketHelper.setSecurity((SSLSocket) socket);
SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) socket, account.getServer().getDomainpart());
SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) socket, "xmpp-client");
SSLSocketHelper.setSecurity((SSLSocket) localSocket);
SSLSocketHelper.setSNIHost(tlsFactoryVerifier.factory, (SSLSocket) localSocket, account.getServer().getDomainpart());
SSLSocketHelper.setAlpnProtocol(tlsFactoryVerifier.factory, (SSLSocket) localSocket, "xmpp-client");
socket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
localSocket.connect(addr, Config.SOCKET_TIMEOUT * 1000);
if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) socket).getSession())) {
if (!tlsFactoryVerifier.verifier.verify(account.getServer().getDomainpart(), ((SSLSocket) localSocket).getSession())) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": TLS certificate verification failed");
throw new SecurityException();
}
}
if (startXmpp())
if (startXmpp(localSocket)) {
break; // successfully connected to server that speaks xmpp
} else {
localSocket.close();
}
} catch (final SecurityException e) {
throw e;
} catch (InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": thread was interrupted before beginning stream");
return;
} catch (final Throwable e) {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage() + "(" + e.getClass().getName() + ")");
if (!iterator.hasNext()) {
@ -385,8 +401,10 @@ public class XmppConnection implements Runnable {
}
}
processStream();
} catch (final java.lang.SecurityException e) {
} catch (final java.lang.SecurityException e) {
this.changeStatus(Account.State.MISSING_INTERNET_PERMISSION);
} catch (final RegistrationNotSupportedException e) {
this.changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
} catch (final IncompatibleServerException e) {
this.changeStatus(Account.State.INCOMPATIBLE_SERVER);
} catch (final SecurityException e) {
@ -410,12 +428,16 @@ public class XmppConnection implements Runnable {
this.changeStatus(Account.State.OFFLINE);
this.attempt = Math.max(0, this.attempt - 1);
} finally {
forceCloseSocket();
if (wakeLock.isHeld()) {
try {
wakeLock.release();
} catch (final RuntimeException ignored) {
if (!Thread.currentThread().isInterrupted()) {
forceCloseSocket();
if (wakeLock.isHeld()) {
try {
wakeLock.release();
} catch (final RuntimeException ignored) {
}
}
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": not force closing socket and releasing wake lock because thread was interrupted");
}
}
}
@ -423,27 +445,18 @@ public class XmppConnection implements Runnable {
/**
* Starts xmpp protocol, call after connecting to socket
* @return true if server returns with valid xmpp, false otherwise
* @throws IOException Unknown tag on connect
* @throws XmlPullParserException Bad Xml
* @throws NoSuchAlgorithmException Other error
*/
private boolean startXmpp() throws IOException, XmlPullParserException, NoSuchAlgorithmException {
private boolean startXmpp(Socket socket) throws Exception {
if (Thread.currentThread().isInterrupted()) {
throw new InterruptedException();
}
this.socket = socket;
tagWriter.setOutputStream(socket.getOutputStream());
tagReader.setInputStream(socket.getInputStream());
tagWriter.beginDocument();
sendStartStream();
Tag nextTag;
while ((nextTag = tagReader.readTag()) != null) {
if (nextTag.isStart("stream")) {
return true;
} else {
throw new IOException("unknown tag on connect");
}
}
if (socket.isConnected()) {
socket.close();
}
return false;
final Tag tag = tagReader.readTag();
return tag != null && tag.isStart("stream");
}
private static class TlsFactoryVerifier {
@ -468,7 +481,8 @@ public class XmppConnection implements Runnable {
} else {
keyManager = null;
}
sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager : trustManager.getNonInteractive()}, mXmppConnectionService.getRNG());
String domain = account.getJid().getDomainpart();
sc.init(keyManager, new X509TrustManager[]{mInteractive ? trustManager.getInteractive(domain) : trustManager.getNonInteractive(domain)}, mXmppConnectionService.getRNG());
final SSLSocketFactory factory = sc.getSocketFactory();
final HostnameVerifier verifier;
if (mInteractive) {
@ -719,7 +733,7 @@ public class XmppConnection implements Runnable {
final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId());
// Packets to the server should have responses from the server
if (packetCallbackDuple.first.toServer(account)) {
if (packet.fromServer(account) || mServerIdentity == Identity.FACEBOOK) {
if (packet.fromServer(account)) {
callback = packetCallbackDuple.second;
packetCallbacks.remove(packet.getId());
} else {
@ -812,10 +826,8 @@ public class XmppConnection implements Runnable {
} else {
throw new IncompatibleServerException();
}
} else if (!this.streamFeatures.hasChild("register")
&& account.isOptionSet(Account.OPTION_REGISTER)) {
forceCloseSocket();
changeStatus(Account.State.REGISTRATION_NOT_SUPPORTED);
} else if (!this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER)) {
throw new RegistrationNotSupportedException();
} else if (this.streamFeatures.hasChild("mechanisms")
&& shouldAuthenticate
&& (features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS)) {
@ -842,6 +854,8 @@ public class XmppConnection implements Runnable {
auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) {
saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("SCRAM-SHA-256")) {
saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("SCRAM-SHA-1")) {
saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG());
} else if (mechanisms.contains("PLAIN")) {
@ -950,7 +964,7 @@ public class XmppConnection implements Runnable {
}
public void resetEverything() {
resetAttemptCount();
resetAttemptCount(true);
resetStreamId();
clearIqCallbacks();
mStanzaQueue.clear();
@ -1073,7 +1087,7 @@ public class XmppConnection implements Runnable {
this.disco.clear();
}
mPendingServiceDiscoveries.set(0);
mWaitForDisco.set(mServerIdentity != Identity.NIMBUZZ && smVersion != 0);
mWaitForDisco.set(smVersion != 0 && !account.getJid().getDomainpart().equalsIgnoreCase("nimbuzz.com"));
lastDiscoStarted = SystemClock.elapsedRealtime();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery");
mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode());
@ -1112,24 +1126,6 @@ public class XmppConnection implements Runnable {
boolean advancedStreamFeaturesLoaded;
synchronized (XmppConnection.this.disco) {
ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet);
for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
if (mServerIdentity == Identity.UNKNOWN && id.getType().equals("im") &&
id.getCategory().equals("server") && id.getName() != null &&
jid.equals(account.getServer())) {
switch (id.getName()) {
case "Prosody":
mServerIdentity = Identity.PROSODY;
break;
case "ejabberd":
mServerIdentity = Identity.EJABBERD;
break;
case "Slack-XMPP":
mServerIdentity = Identity.SLACK;
break;
}
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server name: " + id.getName());
}
}
if (jid.equals(account.getServer())) {
mXmppConnectionService.databaseBackend.insertDiscoveryResult(result);
}
@ -1259,7 +1255,7 @@ public class XmppConnection implements Runnable {
}
private String nextRandomId() {
return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
return new BigInteger(50, mXmppConnectionService.getRNG()).toString(36);
}
public String sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
@ -1359,31 +1355,10 @@ public class XmppConnection implements Runnable {
}
}
public void waitForPush() {
if (tagWriter.isActive()) {
tagWriter.finish();
new Thread(new Runnable() {
@Override
public void run() {
try {
while(!tagWriter.finished()) {
Thread.sleep(10);
}
socket.close();
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream");
changeStatus(Account.State.OFFLINE);
} catch (IOException | InterruptedException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": error while closing socket for waitForPush()");
}
}
}).start();
} else {
forceCloseSocket();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)");
}
}
private void forceCloseSocket() {
if (tagWriter != null) {
tagWriter.forceClose();
}
if (socket != null) {
try {
socket.close();
@ -1403,7 +1378,6 @@ public class XmppConnection implements Runnable {
interrupt();
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force));
if (force) {
tagWriter.forceClose();
forceCloseSocket();
} else {
if (tagWriter.isActive()) {
@ -1526,9 +1500,11 @@ public class XmppConnection implements Runnable {
this.sendPacket(new InactivePacket());
}
public void resetAttemptCount() {
public void resetAttemptCount(boolean resetConnectTime) {
this.attempt = 0;
this.lastConnect = 0;
if (resetConnectTime) {
this.lastConnect = 0;
}
}
public void setInteractive(boolean interactive) {
@ -1536,7 +1512,25 @@ public class XmppConnection implements Runnable {
}
public Identity getServerIdentity() {
return mServerIdentity;
synchronized (this.disco) {
ServiceDiscoveryResult result = disco.get(account.getJid().toDomainJid());
if (result == null) {
return Identity.UNKNOWN;
}
for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) {
if (id.getType().equals("im") && id.getCategory().equals("server") && id.getName() != null) {
switch (id.getName()) {
case "Prosody":
return Identity.PROSODY;
case "ejabberd":
return Identity.EJABBERD;
case "Slack-XMPP":
return Identity.SLACK;
}
}
}
}
return Identity.UNKNOWN;
}
private class UnauthorizedException extends IOException {
@ -1567,6 +1561,10 @@ public class XmppConnection implements Runnable {
}
private class RegistrationNotSupportedException extends IOException {
}
public enum Identity {
FACEBOOK,
SLACK,
@ -1685,6 +1683,10 @@ public class XmppConnection implements Runnable {
return -1;
}
}
public boolean stanzaIds() {
return hasDiscoFeature(account.getJid().toBareJid(),Xmlns.STANZA_IDS);
}
}
private IqGenerator getIqGenerator() {

View file

@ -21,6 +21,8 @@ public final class Jid {
private final String domainpart;
private final String resourcepart;
private static final char[] JID_ESCAPING_CHARS = {' ','"','&','\'','/',':','<','>','@','\\'};
// It's much more efficient to store the ful JID as well as the parts instead of figuring them
// all out every time (since some characters are displayed but aren't used for comparisons).
private final String displayjid;
@ -29,6 +31,18 @@ public final class Jid {
return localpart;
}
public String getUnescapedLocalpart() {
if (localpart == null || !localpart.contains("\\")) {
return localpart;
} else {
String localpart = this.localpart;
for(char c : JID_ESCAPING_CHARS) {
localpart = localpart.replace(String.format ("\\%02x", (int)c),String.valueOf(c));
}
return localpart;
}
}
public String getDomainpart() {
return IDN.toUnicode(domainpart);
}

View file

@ -388,7 +388,7 @@ public class JingleConnection implements Transferable {
long size = Long.parseLong(fileSize.getContent());
message.setBody(Long.toString(size));
conversation.add(message);
mXmppConnectionService.updateConversationUi();
mJingleConnectionManager.updateConversationUi(true);
if (mJingleConnectionManager.hasStoragePermission()
&& size < this.mJingleConnectionManager.getAutoAcceptFileSize()
&& mXmppConnectionService.isDataSaverDisabled()) {
@ -510,7 +510,7 @@ public class JingleConnection implements Transferable {
private void sendAccept() {
mJingleStatus = JINGLE_STATUS_ACCEPTED;
this.mStatus = Transferable.STATUS_DOWNLOADING;
mXmppConnectionService.updateConversationUi();
this.mJingleConnectionManager.updateConversationUi(true);
this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() {
@Override
public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) {
@ -620,6 +620,10 @@ public class JingleConnection implements Transferable {
if (cid != null) {
Log.d(Config.LOGTAG, "candidate used by counterpart:" + cid);
JingleCandidate candidate = getCandidate(cid);
if (candidate == null) {
Log.d(Config.LOGTAG,"could not find candidate with cid="+cid);
return false;
}
candidate.flagAsUsedByCounterpart();
this.receivedCandidate = true;
if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
@ -752,7 +756,7 @@ public class JingleConnection implements Transferable {
}
private void sendFallbackToIbb() {
Log.d(Config.LOGTAG, "sending fallback to ibb");
Log.d(Config.LOGTAG, account.getJid().toBareJid()+": sending fallback to ibb");
JinglePacket packet = this.bootstrapPacket("transport-replace");
Content content = new Content(this.contentCreator, this.contentName);
this.transportId = this.mJingleConnectionManager.nextRandomId();
@ -763,6 +767,18 @@ public class JingleConnection implements Transferable {
this.sendJinglePacket(packet);
}
OnTransportConnected onIbbTransportConnected = new OnTransportConnected() {
@Override
public void failed() {
Log.d(Config.LOGTAG, "ibb open failed");
}
@Override
public void established() {
JingleConnection.this.transport.send(file, onFileTransmissionSatusChanged);
}
};
private boolean receiveFallbackToIbb(JinglePacket packet) {
Log.d(Config.LOGTAG, "receiving fallack to ibb");
String receivedBlockSize = packet.getJingleContent().ibbTransport()
@ -775,13 +791,28 @@ public class JingleConnection implements Transferable {
}
this.transportId = packet.getJingleContent().getTransportId();
this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
this.transport.receive(file, onFileTransmissionSatusChanged);
JinglePacket answer = bootstrapPacket("transport-accept");
Content content = new Content("initiator", "a-file-offer");
content.setTransportId(this.transportId);
content.ibbTransport().setAttribute("block-size",this.ibbBlockSize);
answer.setContent(content);
this.sendJinglePacket(answer);
if (initiator.equals(account.getJid())) {
this.sendJinglePacket(answer, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + " recipient ACKed our transport-accept. creating ibb");
transport.connect(onIbbTransportConnected);
}
}
});
} else {
this.transport.receive(file, onFileTransmissionSatusChanged);
this.sendJinglePacket(answer);
}
return true;
}
@ -796,19 +827,13 @@ public class JingleConnection implements Transferable {
}
}
this.transport = new JingleInbandTransport(this, this.transportId, this.ibbBlockSize);
this.transport.connect(new OnTransportConnected() {
@Override
public void failed() {
Log.d(Config.LOGTAG, "ibb open failed");
}
@Override
public void established() {
JingleConnection.this.transport.send(file,
onFileTransmissionSatusChanged);
}
});
//might be receive instead if we are not initiating
if (initiator.equals(account.getJid())) {
this.transport.connect(onIbbTransportConnected);
} else {
this.transport.receive(file,onFileTransmissionSatusChanged);
}
return true;
} else {
return false;
@ -838,7 +863,7 @@ public class JingleConnection implements Transferable {
if (this.file!=null) {
file.delete();
}
this.mXmppConnectionService.updateConversationUi();
this.mJingleConnectionManager.updateConversationUi(true);
} else {
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_FAILED);
@ -864,7 +889,7 @@ public class JingleConnection implements Transferable {
if (this.file!=null) {
file.delete();
}
this.mXmppConnectionService.updateConversationUi();
this.mJingleConnectionManager.updateConversationUi(true);
} else {
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_FAILED,
@ -1012,7 +1037,7 @@ public class JingleConnection implements Transferable {
public void updateProgress(int i) {
this.mProgress = i;
mXmppConnectionService.updateConversationUi();
mJingleConnectionManager.updateConversationUi(false);
}
public String getTransportId() {

View file

@ -142,6 +142,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
} else if (packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
payload = packet.findChild("data", "http://jabber.org/protocol/ibb");
sid = payload.getAttribute("sid");
} else if (packet.hasChild("close","http://jabber.org/protocol/ibb")) {
payload = packet.findChild("close", "http://jabber.org/protocol/ibb");
sid = payload.getAttribute("sid");
}
if (sid != null) {
for (JingleConnection connection : connections) {

View file

@ -47,7 +47,9 @@ public class JingleInbandTransport extends JingleTransport {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (connected && packet.getType() == IqPacket.TYPE.RESULT) {
sendNextBlock();
if (remainingSize > 0) {
sendNextBlock();
}
}
}
};
@ -60,6 +62,14 @@ public class JingleInbandTransport extends JingleTransport {
this.sessionId = sid;
}
private void sendClose() {
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart);
Element close = iq.addChild("close", "http://jabber.org/protocol/ibb");
close.setAttribute("sid", this.sessionId);
this.account.getXmppConnection().sendIqPacket(iq, null);
}
public void connect(final OnTransportConnected callback) {
IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.setTo(this.counterpart);
@ -155,6 +165,7 @@ public class JingleInbandTransport extends JingleTransport {
try {
int count = fileInputStream.read(buffer);
if (count == -1) {
sendClose();
file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
fileInputStream.close();
@ -181,12 +192,13 @@ public class JingleInbandTransport extends JingleTransport {
if (this.remainingSize > 0) {
connection.updateProgress((int) ((((double) (this.fileSize - this.remainingSize)) / this.fileSize) * 100));
} else {
sendClose();
file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest()));
this.onFileTransmissionStatusChanged.onFileTransmitted(file);
fileInputStream.close();
}
} catch (IOException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": "+e.getMessage());
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": io exception during sendNextBlock() "+e.getMessage());
FileBackend.close(fileInputStream);
this.onFileTransmissionStatusChanged.onFileTransferAborted();
}
@ -232,7 +244,13 @@ public class JingleInbandTransport extends JingleTransport {
this.receiveNextBlock(payload.getContent());
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null);
} else if (connected && payload.getName().equals("close")) {
this.connected = false;
this.account.getXmppConnection().sendIqPacket(
packet.generateResponse(IqPacket.TYPE.RESULT), null);
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received ibb close");
} else {
Log.d(Config.LOGTAG,payload.toString());
// TODO some sort of exception
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 368 B

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 371 B

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 750 B

After

Width:  |  Height:  |  Size: 750 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 757 B

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 779 B

After

Width:  |  Height:  |  Size: 779 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 687 B

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 707 B

Some files were not shown because too many files have changed in this diff Show more