Compare commits

...

844 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
Daniel Gultsch
1e4b1a3346 version bump to 1.14.7 + changelog 2016-10-26 12:28:18 +02:00
Daniel Gultsch
8557120ef8 add error message to failed messages. accessible via context menu 2016-10-26 12:26:04 +02:00
Daniel Gultsch
a4020e85f6 use file provider on android M as well 2016-10-23 09:03:36 +02:00
Daniel Gultsch
8c1bb058da connect instantly in low ping mode after going offline 2016-10-23 09:03:17 +02:00
Daniel Gultsch
10398cab51 don't leave low timeout mode prematurely 2016-10-20 20:04:16 +02:00
Daniel Gultsch
f2696b66ba Merge branch 'feature-remove-merge-separator' of https://github.com/Mishiranu/Conversations into Mishiranu-feature-remove-merge-separator 2016-10-20 18:18:25 +02:00
Daniel Gultsch
52d4be4249 Merge branch 'feature-remove-spans' of https://github.com/Mishiranu/Conversations into Mishiranu-feature-remove-spans 2016-10-20 18:10:52 +02:00
Daniel Gultsch
0f62ff6736 introduced low ping timeout mode after gcm push 2016-10-20 18:02:11 +02:00
Daniel Gultsch
44ce5df359 write prepped string to db. use display version everywhere else 2016-10-20 17:31:46 +02:00
Mishiranu
fd4e15ba97 Remove MERGE_SEPARATOR 2016-10-20 01:03:51 +03:00
Mishiranu
8835f08cf7 Remove spans on copying or pasting a text 2016-10-19 20:47:41 +03:00
Daniel Gultsch
c3423d6ffe include pgp signature only in non anonymous mucs 2016-10-19 12:31:11 +02:00
Daniel Gultsch
dce8149aae retrigger key selection if openpgp key was deleted 2016-10-19 11:53:55 +02:00
Daniel Gultsch
7226fc0010 update conversation in database background thread 2016-10-18 13:06:24 +02:00
Daniel Gultsch
50780debf7 don't trigger context menu in message adapter manually. fixes #2077 2016-10-18 11:16:43 +02:00
Daniel Gultsch
f8c21caec9 Merge branch 'feature-selection' of https://github.com/Mishiranu/Conversations into Mishiranu-feature-selection 2016-10-17 09:53:32 +02:00
Daniel Gultsch
22d13a3dcd add exception handling when loading default resource 2016-10-17 09:53:08 +02:00
Daniel Gultsch
dc02e2b498 small code reformation in pgp decryption service 2016-10-17 09:52:43 +02:00
Daniel Gultsch
6371d2b7a9 Merge pull request #2063 from thacoon/patch-1
Fix OpenPGP link
2016-10-13 12:21:54 +02:00
Daniel Gultsch
2a73b8d76e clarified fineprint a little bit 2016-10-13 12:17:20 +02:00
Daniel Gultsch
f6cfa27741 synchronize access to json key storage in account model 2016-10-13 11:27:26 +02:00
Daniel Gultsch
501152bcfd version bump to 1.14.6 + changelog 2016-10-10 17:54:34 +02:00
Daniel Gultsch
9e54fd5c92 don't use sending state on muc pms without smacks 2016-10-09 19:40:30 +02:00
Daniel Gultsch
cd1c05a7c3 add password to direct muc invite 2016-10-09 19:40:03 +02:00
Daniel Gultsch
f7d51b8890 pulled more translations from transifex 2016-10-09 18:06:19 +02:00
Daniel Gultsch
c5bdb04490 pulled translations from transifex 2016-10-09 11:13:45 +02:00
Daniel Gultsch
74087b873f added disclaimer that conversations.im account is 8 euro / year 2016-10-08 18:24:20 +02:00
Constantin Soffner
ad8b9eb054 Fix OpenPGP link
I have just updated the link to the OpenPGP website.
2016-10-08 14:40:13 +02:00
Daniel Gultsch
f3ef8d4978 fetch new conference configuration on every conf update 2016-10-08 12:10:53 +02:00
Daniel Gultsch
9efef24a04 reset sending to waiting on every error 2016-10-07 14:54:35 +02:00
Daniel Gultsch
5a73a6b139 fixed account hash calculation 2016-10-07 14:54:06 +02:00
Daniel Gultsch
1f7f82da7b respond to chat marker request only when mutual presence subscription exists 2016-10-07 10:05:08 +02:00
Daniel Gultsch
26e33de79a create new instances of key manager every time it's used 2016-10-07 10:04:36 +02:00
Daniel Gultsch
187825d6c6 warn user if account is offline during avatar publication 2016-10-06 22:06:09 +02:00
Daniel Gultsch
6d5f23213b refresh error notification after 'try again' 2016-10-06 22:05:40 +02:00
Daniel Gultsch
0af13fc746 be more careful parsing integers in omemo 2016-10-06 22:05:18 +02:00
Daniel Gultsch
5530b0b0e2 Merge branch 'master' of github.com:siacs/Conversations 2016-10-06 18:39:19 +02:00
Daniel Gultsch
40e5090bdd issue ping after push was received 2016-10-06 18:09:55 +02:00
Daniel Gultsch
9f060f477f parse smacks delay from messages 2016-10-06 18:09:44 +02:00
Daniel Gultsch
8d8cb92e43 try to fix messages stuck at sending 2016-10-06 17:23:35 +02:00
Daniel Gultsch
27af6a4b1e Add client recommandition to readme. fixes #2048 2016-10-05 09:05:53 +02:00
Daniel Gultsch
082c06a486 make error notification dismissable. fixes #1815 2016-10-04 11:16:59 +02:00
Daniel Gultsch
5ac0e9267d fixed omemo shown as unavailable in 1:1 chats 2016-10-03 21:04:10 +02:00
Daniel Gultsch
cea52b0722 resolve take photo uri for internal use 2016-10-03 18:26:11 +02:00
Daniel Gultsch
f4a883848c properly index take photo uris from file provider 2016-10-03 11:25:15 +02:00
Daniel Gultsch
b6e7def9db add more logging to attaching file process 2016-10-03 11:13:04 +02:00
Daniel Gultsch
7c6d1d19d5 when activating omemo in conference always check preferences 2016-10-03 10:42:43 +02:00
Daniel Gultsch
dcd6ef8f84 explicit logging when copying files to storage 2016-10-03 10:13:45 +02:00
Daniel Gultsch
d6c2ff9782 version bump to 1.14.5 + changelog 2016-10-01 15:49:09 +02:00
Daniel Gultsch
b0fb9fd9ee added nick to conference jid example 2016-09-28 13:20:52 +02:00
Daniel Gultsch
e275fd8143 Merge pull request #2027 from danielegobbetti/wear-reply-dismiss-notification
Dismiss the notification when replying from a wear notification [needs review!]
2016-09-28 12:39:00 +02:00
Daniel Gultsch
43f5dfe174 simplified code that invokes the export logs service 2016-09-28 12:35:52 +02:00
Daniel Gultsch
f0dbcce58f expert 'setting' to remove omemo identity. fixes #2038 2016-09-28 12:24:50 +02:00
Daniele Gobbetti
41db773b08 Allow to dismiss the notification from a wear reply.
- use different IDs in the same method for the PendingIntent
- fix reply for GPG encrypted replies (untested)
2016-09-27 17:39:23 +02:00
Daniel Gultsch
5cd8917122 remove dubplicate play service dependency from build 2016-09-27 11:45:11 +02:00
Daniel Gultsch
bb48f67a30 always use ipv4 localhost for Orbot connections 2016-09-27 11:44:50 +02:00
Daniel Gultsch
1339b9c464 don't reset encryption choice to auto on archiving 2016-09-24 21:29:00 +02:00
Daniel Gultsch
cee3c98a23 version bump to 1.14.4 + changelog 2016-09-24 21:24:55 +02:00
Daniel Gultsch
343d895a26 don't react to null and empty voice replies 2016-09-21 19:04:16 +02:00
Daniel Gultsch
13ed27f91e don't use file provider for photo uris on android < N. fixes #2030 2016-09-21 18:20:53 +02:00
Daniel Gultsch
401759cdc7 don't wait for disco when not having stream managment 2016-09-21 12:55:40 +02:00
Daniel Gultsch
61f58b3dbd add timeouts to HTTPUrlConnections and allow cancelation of all sending files 2016-09-20 20:02:25 +02:00
Daniel Gultsch
de7c0c5121 Merge pull request #2028 from Mishiranu/feature-more-tables
Fix "Server info" table layout
2016-09-20 16:43:51 +02:00
Mishiranu
9aaa5b78f4 Update Russian translation 2016-09-20 16:15:46 +03:00
Mishiranu
18ab826413 Fix "More table" layout
Retain "More table" visibility on screen orientation change
2016-09-20 16:10:25 +03:00
Daniel Gultsch
5790d4c4ab fixed styling in blocking dialog 2016-09-20 14:21:41 +02:00
Daniel Gultsch
98ab9beec7 version bump to 1.14.3 + changelog 2016-09-20 11:25:33 +02:00
Daniel Gultsch
7bda624723 pulled translations from transifex 2016-09-20 11:22:26 +02:00
Daniel Gultsch
7eac903277 add support for XEP-0377: Spam Reporting 2016-09-18 23:21:05 +02:00
Daniel Gultsch
badc97e280 don't simply ignore null in message body but try to avoid it 2016-09-18 22:15:02 +02:00
Daniel Gultsch
7c608c8862 recreate activities when theme changed 2016-09-18 20:26:47 +02:00
Daniel Gultsch
6b904d4de1 use proper paddings in dialogs on android < 5 2016-09-18 20:09:39 +02:00
Mishiranu
858a327299 Retain TextView selection after list updating 2016-09-18 16:35:14 +03:00
Daniel Gultsch
7bdd4166c0 catch all throwables when loading contacts 2016-09-17 11:31:35 +02:00
Daniel Gultsch
00c04cd413 version bump to 1.14.2 + changelog 2016-09-17 11:30:52 +02:00
Mishiranu
3e6747c880 Add "Select text" context menu option 2016-09-17 01:18:34 +03:00
Daniel Gultsch
9d0a333372 Merge branch 'master' of github.com:siacs/Conversations 2016-09-16 12:35:57 +02:00
Daniel Gultsch
af55aeca58 pulled translations from transifex 2016-09-16 12:29:26 +02:00
Daniel Gultsch
521469a57d dont show delete file button when outside conversations directory. fixes #2007 2016-09-16 12:29:12 +02:00
Daniel Gultsch
569b7bf6d0 hint that you should use latest version of ejabberd 2016-09-16 11:59:46 +02:00
Daniel Gultsch
3cdf5f9afc Merge branch 'Mishiranu-master' 2016-09-16 11:14:06 +02:00
Daniel Gultsch
15c807730e Merge branch 'master' of https://github.com/Mishiranu/Conversations into Mishiranu-master 2016-09-16 11:08:37 +02:00
Daniel Gultsch
7b445bc4c7 use history clear date as minimum date for mam 2016-09-16 11:07:52 +02:00
Mishiranu
8ca5eb4429 Allow text selection with multiple links in message 2016-09-16 02:15:07 +03:00
Daniel Gultsch
ab63dba8aa deal with null bodys in message preview 2016-09-15 18:51:51 +02:00
Daniel Gultsch
4359afacb4 store jid if it was changed during bind 2016-09-14 12:26:38 +02:00
Daniel Gultsch
7b52e6984c Merge pull request #2018 from SamWhited/sasl_anonymous
SASL ANONYMOUS (no UI)
2016-09-14 09:34:20 +02:00
Daniel Gultsch
869ee3d438 Merge branch 'pebble-notification' of https://github.com/danielegobbetti/Conversations into danielegobbetti-pebble-notification 2016-09-12 22:49:22 +02:00
Daniel Gultsch
d3dfecae8a don't use display version of jids 2016-09-12 22:48:51 +02:00
Daniel Gultsch
6cb2b0b5d1 remember scroll position on rotate. fixes #2011 2016-09-12 21:18:56 +02:00
Sam Whited
1a0b538166 Use JID returned by the server during bind
Not just the resourcepart
2016-09-12 11:33:36 -05:00
Sam Whited
805717673c Support ANONYMOUS SASL 2016-09-12 11:30:03 -05:00
Daniele Gobbetti
e6e46651c9 Use the last message in the content text instead of the first.
This fixes the issue where the first message in the notification was sent to pebble
(and possibly to other wear devices) for every update in the conversation, as
reported in #1249.

This is the same patch propoed in https://github.com/siacs/Conversations/issues/1249#issuecomment-245878335
2016-09-11 18:42:05 +02:00
Daniel Gultsch
75fcab3170 Merge pull request #2014 from licaon-kter/patch-1
Fix typo
2016-09-11 17:40:27 +02:00
licaon-kter
59b2e281a3 Fix typo 2016-09-10 22:16:14 +03:00
Daniel Gultsch
6fd9888b3b add xep-0084 to docs/xep.md. wikipedia was complaining about it missing 2016-09-10 16:24:41 +02:00
Daniel Gultsch
c3b11e515e download own vcard avatar if none is set. fixes #2008 2016-09-09 11:04:05 +02:00
Daniel Gultsch
edf0ae9aa6 version bump to 1.14.1 + changelog 2016-09-09 11:03:22 +02:00
Daniel Gultsch
00cbf8458a pulled translations from transifex 2016-09-08 11:19:03 +02:00
Daniel Gultsch
ac9f13a9f2 provide hint on why conference can not be encrypted 2016-09-08 11:01:27 +02:00
Daniel Gultsch
a54a7dca30 Merge branch 'master' of github.com:siacs/Conversations 2016-09-07 16:14:33 +02:00
Daniel Gultsch
416481bb65 be a bit more careful when deleting and deactivating accounts 2016-09-07 14:34:58 +02:00
Daniel Gultsch
ba6b4763d2 add faq about grayed out omemo 2016-09-07 13:01:02 +02:00
Daniel Gultsch
e1d2c32e63 show server not found muc error 2016-09-06 12:15:08 +02:00
Daniel Gultsch
257d1e42d8 remove explicit pebble support. treat as wear device 2016-09-06 12:14:49 +02:00
Daniel Gultsch
7e81149869 show reply action on wear devices 2016-09-06 12:13:50 +02:00
Daniel Gultsch
1dc55f72e3 don't use fileprovider when opening files on android M and below 2016-09-04 22:59:40 +02:00
Daniel Gultsch
d2c475d501 don't crash when correcting waiting pgp encrypted messages 2016-09-04 22:59:15 +02:00
Daniel Gultsch
ad09d7dc49 version bump to 1.14.0 2016-09-02 23:49:32 +02:00
Daniel Gultsch
aca7054174 fixed recreation issues in StartConversationActivity 2016-08-31 17:04:43 +02:00
Daniel Gultsch
f7d8580969 fixed typo 2016-08-31 17:02:50 +02:00
Daniel Gultsch
f14ab4c391 don't show duplicate nofications on android 4 2016-08-31 17:02:42 +02:00
Daniel Gultsch
7917c19d18 broader exception catchers 2016-08-30 13:15:00 +02:00
Daniel Gultsch
3685c8cd2a use file provider for taking pictures 2016-08-30 13:14:38 +02:00
Daniel Gultsch
d32cbcc70d don't show up navigation in startConversation when there are no open conversations 2016-08-30 13:13:06 +02:00
Daniel Gultsch
af329eff46 add more logging to pgp engine 2016-08-30 13:12:09 +02:00
Daniel Gultsch
b747afb44c version bump to 1.14.0-beta + changelog 2016-08-27 15:30:41 +02:00
Daniel Gultsch
2c187d0e7c mark conversation as read when swiping a notification with quick reply away 2016-08-27 15:25:37 +02:00
Daniel Gultsch
caafd03130 don't automatically download files and avatars when datasaver is on 2016-08-27 13:35:52 +02:00
Daniel Gultsch
3d5940cb76 bring back connectivity changed events on android n 2016-08-27 12:15:25 +02:00
Daniel Gultsch
78e962ce67 don't overwrite edited information in editaccount on rotate 2016-08-26 21:48:14 +02:00
Daniel Gultsch
ea0e6d0619 don't set notification mode to background when on pause 2016-08-26 21:13:33 +02:00
Daniel Gultsch
ad994a2f4c add missing depency to gradle file 2016-08-26 16:43:30 +02:00
Daniel Gultsch
fd54dc5aff wrap dynamic tags into multiple lines. fixes #2003 2016-08-26 16:34:42 +02:00
Daniel Gultsch
76cbb4f727 some multi-window optimizations. set min width to 300 2016-08-26 16:05:38 +02:00
Daniel Gultsch
e33d8451a8 attach contact to notification 2016-08-26 13:35:01 +02:00
Daniel Gultsch
f931c08da7 add snackbar for request presence subscription 2016-08-26 10:19:59 +02:00
Daniel Gultsch
b52f079292 always display allow contact perm dialog after intro 2016-08-26 10:19:42 +02:00
Daniel Gultsch
9e0145a8f6 survive rotation in editaccount 2016-08-26 09:10:59 +02:00
Daniel Gultsch
e98ab37c9d made payment required error standard compliant 2016-08-25 23:42:42 +02:00
Daniel Gultsch
cbda5a5016 reformat build.gradle 2016-08-25 22:53:27 +02:00
Daniel Gultsch
910b38ec13 add file provider to share files on android n 2016-08-25 22:41:33 +02:00
Daniel Gultsch
b0cdc2745c fix travis 2016-08-25 19:35:37 +02:00
Daniel Gultsch
2e4713897d offer quick reply on android N 2016-08-25 17:30:44 +02:00
Daniel Gultsch
542626758d use N style stacked notifications 2016-08-25 15:20:06 +02:00
Daniel Gultsch
a4d342683e use N Api and build tools 2016-08-25 15:19:51 +02:00
Daniel Gultsch
0b9d38cf32 send register IQs without full from 2016-08-25 13:50:54 +02:00
Daniel Gultsch
f1ecbf2ff8 made image preview size smaller for low res images 2016-08-25 13:46:33 +02:00
Daniel Gultsch
6f72128c45 version bump to 1.13.9 + changelog 2016-08-20 10:45:14 +02:00
Daniel Gultsch
8927ba8065 various null pointer checks 2016-08-20 10:44:50 +02:00
Daniel Gultsch
a0038565c5 pulled translations from transifex 2016-08-19 21:47:51 +02:00
Daniel Gultsch
285d86b375 display error status for missing internet permission 2016-08-19 21:47:08 +02:00
Daniel Gultsch
cf909afc60 check for rare null pointer platform bug in share intent handling 2016-08-19 21:46:47 +02:00
Daniel Gultsch
2a139a4b47 Merge pull request #1987 from licaon-kter/patch-2
Make 'export log' option function clearer
2016-08-16 10:49:04 +02:00
Daniel Gultsch
0528a47b8a pulled translations from transifex 2016-08-16 10:45:52 +02:00
Daniel Gultsch
b5d3859b22 add payment required error 2016-08-16 10:39:59 +02:00
Daniel Gultsch
343bb7ff28 don't close otr session on every presence change 2016-08-13 12:43:06 +02:00
Daniel Gultsch
94aee445e7 start file observer in background 2016-08-13 12:40:48 +02:00
Daniel Gultsch
4736d12e99 make lastMessageTransmitted return max(clear_date,last_message) 2016-08-13 12:36:30 +02:00
Daniel Gultsch
eb8b6165d7 be more careful in recursive file observer. limit depth 2016-08-13 12:35:10 +02:00
licaon-kter
81b0f60860 Clear export option function 2016-08-11 21:07:25 +03:00
Daniel Gultsch
8b6f06f0f9 version bump to 1.13.8 + changelog 2016-08-11 10:01:41 +02:00
Daniel Gultsch
08725ba2bb use direct ssl when port was manually set to 5223
this should create a work around for the oracle xmpp server
2016-08-10 12:34:05 +02:00
Daniel Gultsch
9bfdbc708e close tcp connection after 30s of inactivity when in push_mode 2016-08-09 19:21:54 +02:00
Daniel Gultsch
856029a611 don't do idle ping if close_tcp option is set 2016-08-09 17:26:18 +02:00
Daniel Gultsch
a51de9fcd9 explictly set account status to offline when waiting for push 2016-08-09 17:25:45 +02:00
Daniel Gultsch
121312d103 catch all throwables when parsing xml 2016-08-02 10:58:54 +02:00
Daniel Gultsch
d02e24248f catch platform bug when getting ringer mode 2016-08-02 10:58:31 +02:00
Daniel Gultsch
8b331895d1 catch NPE in getVideoPreview() because getFrame sometimes returns null 2016-08-02 10:57:50 +02:00
Daniel Gultsch
ed2fa20414 handle invalid prekey ids in bundle 2016-08-02 10:40:24 +02:00
Daniel Gultsch
9dc8e3db9d set tablet, phone or pc identity 2016-07-31 22:32:51 +02:00
Daniel Gultsch
1b114beb0b add logging when swiping away from recents is being ignored 2016-07-31 22:32:10 +02:00
Daniel Gultsch
3c48b14448 catch exception when trying to get video preview of pgp encrypted file 2016-07-31 22:31:47 +02:00
Daniel Gultsch
0e96e0a796 show identity type for device selection 2016-07-28 22:58:37 +02:00
Daniel Gultsch
c06aceaae9 version bump to 1.13.7 + changelog 2016-07-28 22:43:43 +02:00
Daniel Gultsch
04976fe333 pulled translations from transifex 2016-07-28 22:41:14 +02:00
Daniel Gultsch
178229ac60 add OS to version response 2016-07-27 20:58:13 +02:00
Daniel Gultsch
dbab43e423 fixed rare null pointer in avatar creation 2016-07-27 20:11:22 +02:00
Daniel Gultsch
cf7df84cab add trillian to html otr parsing fixes #1963 2016-07-27 20:11:02 +02:00
Daniel Gultsch
701140fe92 pulled translations from transifex 2016-07-27 20:01:09 +02:00
Daniel Gultsch
58a3ef46ce fixed regression in file delete detection due to missing ! 2016-07-26 20:44:28 +02:00
Daniel Gultsch
82908fb54b added more logging for file deletion 2016-07-26 20:43:05 +02:00
Daniel Gultsch
3409399ef1 display specific error message when password is too weak on registration 2016-07-25 15:57:47 +02:00
Daniel Gultsch
198a9f2226 refactored how view intents are handled
processing view intents before saved instance caused troubles when the activity was destroyed
fixes #1969
2016-07-25 14:16:09 +02:00
Daniel Gultsch
89a05265ea refactored deleted file detection to monitor entire sd card. fixes #1968 2016-07-23 16:12:45 +02:00
Daniel Gultsch
3d372cb339 feed version response from app name instead of static variable 2016-07-22 18:22:21 +02:00
Daniel Gultsch
6dcce76568 don't crash when opening 'xmpp:' uris 2016-07-22 16:33:09 +02:00
Daniel Gultsch
3a5735e717 provide black background video thumbnail if preview couldn't be generated 2016-07-22 16:32:38 +02:00
Daniel Gultsch
e9c00c0427 push file offered notification when initial HTTP HEAD req. fails 2016-07-21 19:17:26 +02:00
Daniel Gultsch
c8188ee52c offer back/cancel button when using magic create 2016-07-21 19:16:41 +02:00
Daniel Gultsch
2843a0af26 announce OTR support as per XEP-0378 2016-07-17 22:51:40 +02:00
Daniel Gultsch
e90e333f29 allow message correction by default since security implications are negligible
Conversations only allows correction of the *last* message. so nudging a message into oblivion by adding a message correction doesn't work. also conversations checks the fingerprint for encrypted messages
2016-07-17 22:42:37 +02:00
Daniel Gultsch
eb3ac1c326 additional null pointer checks when verifying otr keys 2016-07-17 22:02:08 +02:00
Daniel Gultsch
3e50d4831f show toast hint when touching inactive omemo fingerprints 2016-07-17 20:31:04 +02:00
Daniel Gultsch
0bc5dbdf94 version bump to 1.13.6 + changelog 2016-07-16 19:38:07 +02:00
Daniel Gultsch
baa149924a show error notification in connecting state as well 2016-07-14 23:23:13 +02:00
Daniel Gultsch
1db85e582e add more error states for stream errors 2016-07-14 17:05:43 +02:00
Daniel Gultsch
2803d342e1 include pgp and omemo fallback message only when unencrypted is enabled 2016-07-14 16:06:05 +02:00
Daniel Gultsch
223d50c1a0 don't take stanza-id into account when deduping muc pms 2016-07-14 09:01:15 +02:00
Daniel Gultsch
27690865a6 respond to XEP-0202: Entity Time 2016-07-13 18:10:10 +02:00
Daniel Gultsch
58d5d2a1be don't time out disco request but just send bind request 2016-07-13 00:20:57 +02:00
Daniel Gultsch
ff1b23b4d9 call update file params from thread
now that file params has more work to do we should make sure we always call it from a sperate thread
2016-07-13 00:20:38 +02:00
Daniel Gultsch
be4aa2afc9 show a preview for video files 2016-07-11 21:24:33 +02:00
Daniel Gultsch
01a4d2ea25 fixed typo in changelog 2016-07-11 11:31:22 +02:00
Daniel Gultsch
f9aca85edf version bump to 1.13.5 + changelog 2016-07-09 13:33:46 +02:00
Daniel Gultsch
57e51bc735 don't crash when tabbing through muc user list with offline users 2016-07-08 13:24:14 +02:00
Daniel Gultsch
cdee91363c simplified muc users ordering 2016-07-04 19:30:19 +02:00
Daniel Gultsch
ac8aa63916 do not crash on jingle connection when contact doesn't use disco 2016-07-04 19:29:46 +02:00
Daniel Gultsch
369e7172d6 version bump to 1.13.4 + changelog 2016-07-02 12:46:14 +02:00
Daniel Gultsch
09aba0a062 pulled translations from transifex 2016-07-01 13:08:44 +02:00
Daniel Gultsch
9efa242d96 use direct invites to re-invite muc members 2016-06-30 23:09:16 +02:00
Daniel Gultsch
30110431ba use dnd as overriding status 2016-06-30 23:08:55 +02:00
Daniel Gultsch
91c3732c63 don't show 'disable foreground service' button. fixes #1933 2016-06-29 17:20:27 +02:00
Daniel Gultsch
f7933c26d7 don't crash on broken base64 in omemo messages. fixes #1934 2016-06-29 17:18:57 +02:00
Daniel Gultsch
1d79a677c8 support jingle ft:4 to be compatible with swift
Conversations and Gajim both have an implementation bug that sends the jingle session id instead of the transport id (compare XEP-260 2.2). This commit has a work around for this that remains buggy when using ft:3. If gajim is ever to fix this we will be incompatbile. gajim should implement ft:4 instead. (gajim to gajim is broken as well)
2016-06-29 17:16:40 +02:00
Daniel Gultsch
b5caa8fa35 don't show 'create conference' toast on invite 2016-06-28 10:34:43 +02:00
Daniel Gultsch
8882c6b6fd parse §5.1.2 full jids from muc archives for OMEMO messages 2016-06-28 10:33:46 +02:00
Daniel Gultsch
e63d6b4bf2 only keep offline members in members only conferences 2016-06-28 10:32:06 +02:00
Daniel Gultsch
9a7f51520e render ic_launcher. fixes #1919 2016-06-28 08:00:04 +02:00
Daniel Gultsch
4e6d16c49b version bump to 1.13.3 2016-06-25 13:07:33 +02:00
Daniel Gultsch
e52f662569 pulled translations from transifex 2016-06-25 13:07:22 +02:00
Daniel Gultsch
72a2622c84 introduced share button in contact details. remove show qr 2016-06-24 15:16:01 +02:00
Daniel Gultsch
97fe14c4be code cleanup in jingle socks5 transport 2016-06-24 13:36:37 +02:00
Daniel Gultsch
78e3afc1af show error toasts on ui thread 2016-06-24 13:36:06 +02:00
Daniel Gultsch
d2ca0c7fe8 catch exceptions when retrieving uri file extension 2016-06-24 13:35:39 +02:00
Daniel Gultsch
4d5e0c291e remove white spaces from hostname 2016-06-22 12:23:11 +02:00
Daniel Gultsch
982a20fef5 refactor code that reads real jid from muc 2016-06-22 12:22:57 +02:00
Daniel Gultsch
4ba5472d0c respond to block list push 2016-06-22 12:22:36 +02:00
Daniel Gultsch
d28d968985 make sure that we always release wake lock even after throwing exception 2016-06-22 12:22:03 +02:00
Daniel Gultsch
34454ef2ec synchronize stanza count increment and write 2016-06-22 12:21:33 +02:00
Daniel Gultsch
4d1640d6ff Merge pull request #1923 from alexxthehood/patch-1
Update create_conference_dialog.xml to also match dark theme.
2016-06-21 23:25:51 +02:00
alexxthehood
e88f01923f Update create_conference_dialog.xml
Updated to the text color attribute so it fits to the bright and dark theme appropriately.
2016-06-21 19:08:38 +02:00
Daniel Gultsch
1166619539 version bump to 1.13.2 + changelog 2016-06-20 15:56:09 +02:00
Daniel Gultsch
28dc888159 display toast on pgp error 2016-06-19 11:08:17 +02:00
Daniel Gultsch
ea1e4c773d add some missing XEPs to docs 2016-06-19 11:07:49 +02:00
Daniel Gultsch
37e7175a86 log reason for not showing notification 2016-06-19 11:04:59 +02:00
Daniel Gultsch
85c82d9b3b remove ascii control chars when creating xml 2016-06-19 00:07:15 +02:00
Daniel Gultsch
829720409d updated screenshots in README. fixes #1580 2016-06-17 14:01:20 +02:00
Daniel Gultsch
f91d16cbe7 don't fail on missing jid in bookmarks 2016-06-16 20:38:35 +02:00
Daniel Gultsch
b92b3863b9 don't handle chat states in muc or from archive 2016-06-16 20:38:02 +02:00
Daniel Gultsch
fc3aefd56e show toast when connection to openkeychain could not be made 2016-06-16 20:37:32 +02:00
Daniel Gultsch
dcc13d7a3d log download failure caused by missing content length 2016-06-16 20:36:51 +02:00
Daniel Gultsch
48a7818e88 mark used otr fingprint in contact details and highlight pgp 2016-06-16 12:12:24 +02:00
Daniel Gultsch
1eb776f39c synchronize message body changes for message correction 2016-06-16 11:47:40 +02:00
Daniel Gultsch
f8b1e8098c extract relevant extension from file name when processing share intent 2016-06-16 11:46:25 +02:00
Daniel Gultsch
60588af825 replace corrected messages in decryption queue 2016-06-15 14:29:25 +02:00
Daniel Gultsch
f99f21ab9b pulled translations from transifex 2016-06-15 14:11:27 +02:00
Daniel Gultsch
5f4471a45e only dismiss sent message after encryption 2016-06-15 13:53:34 +02:00
Daniel Gultsch
cb5393c32f refresh UI to redraw message hint after switching to pgp 2016-06-15 13:52:49 +02:00
Daniel Gultsch
5f40a7042d delay notification until after pgp decryption 2016-06-15 12:44:29 +02:00
Daniel Gultsch
e0575642b5 log all fail reasons 2016-06-15 12:33:59 +02:00
Daniel Gultsch
73679b97f1 show xep-0172 nick only for contacts with mutual presence subscription 2016-06-15 09:44:01 +02:00
Daniel Gultsch
49de43b364 clear muc tiles when avatar of member changes 2016-06-14 17:11:31 +02:00
Daniel Gultsch
f9600b950f sort muc users by affiliation, name. fixes #1913 2016-06-14 14:41:32 +02:00
Daniel Gultsch
95a51ea2e0 synchronize access to stanza queue 2016-06-14 10:17:37 +02:00
Daniel Gultsch
39ad426ca9 remove messages from decryption queue when trimming a conversation 2016-06-13 19:06:09 +02:00
Daniel Gultsch
40f81f19df make sure tagwriter is clear before force closing socket 2016-06-13 19:05:32 +02:00
Daniel Gultsch
587fb3cca3 refactored pgp decryption 2016-06-13 13:32:14 +02:00
Daniel Gultsch
490a1ca3cf version bump to 1.13.1 + changelog 2016-06-13 12:32:49 +02:00
Daniel Gultsch
ea667a1a73 pulled translations from transifex 2016-06-12 14:49:21 +02:00
Daniel Gultsch
f4e3cd5098 actually do add fall back message for omemo 2016-06-12 14:49:04 +02:00
Daniel Gultsch
c4680e3198 make text color of last-seen match theme 2016-06-12 13:15:28 +02:00
Daniel Gultsch
31dd7b5a21 parse real jid from muc mam messages. (disabled)
parsing this is dangerous if server doesn't filter properly
thus it is disabled in config
2016-06-12 12:50:53 +02:00
Daniel Gultsch
74d376be68 close db cursor after reading cert 2016-06-12 12:50:31 +02:00
Daniel Gultsch
5017e8564c made background color of swiped conversations darker 2016-06-10 23:22:16 +02:00
Daniel Gultsch
a70f57358e use darker green as background for chat bubbles in dark theme 2016-06-10 22:39:02 +02:00
Daniel Gultsch
4bf9a1e809 use darker colors for actionbar on dark theme 2016-06-10 20:15:09 +02:00
Daniel Gultsch
e2a803ee04 version bump to 1.13.0 + changelog 2016-06-10 11:15:12 +02:00
Daniel Gultsch
4b9b7257a9 pulled translations from transifex 2016-06-09 21:00:51 +02:00
Daniel Gultsch
cb7c47bc62 catch conversations sort exception. not vital at this point 2016-06-09 14:50:13 +02:00
Daniel Gultsch
33a02faad9 fixed spelling in last activity summary 2016-06-08 21:36:29 +02:00
Daniel Gultsch
a018935b23 pulled translations from transifex 2016-06-08 20:17:10 +02:00
Daniel Gultsch
112a4d389e Merge branch 'Wanztwurst-darkTheme' fixes #529 2016-06-08 20:10:21 +02:00
Steffen Keiper
7932244c51 Dark theme, theme switch, icons, style, strings
added some white icons,
changed hardcoded icons to theme attributes,
changed icon_edit_dark to icon_edit_body to reflect icons position,
grey message bubbles in dark theme,
misc

purged ic_action_chat as it wasn't used

preference use_white_background changed to use_green_background, default true

grey chat bubbles darker, text white

replaced all grey600 with black icons and 0.54 alpha attribute

highlightColor in dark grey chat bubble now darker than background
2016-06-08 20:07:40 +02:00
Daniel Gultsch
b88128241e Merge pull request #1895 from pp3345/right-alt
Do not treat Right Alt key as a modifier for key combos
2016-06-05 23:19:56 +02:00
Daniel Gultsch
9f42ead747 spelling in readme 2016-06-05 23:19:03 +02:00
Yussuf Khalil
92bad0fa1e Do not treat right alt key as a modifier for key combos 2016-06-05 20:21:44 +02:00
Daniel Gultsch
d089ceac13 add paragraph on running your own server to readme 2016-06-05 12:04:49 +02:00
Daniel Gultsch
8e6f054e52 make non interactive verfier non interactive 2016-06-05 11:56:56 +02:00
Daniel Gultsch
36ae840d76 log all background stanzas when background logging is enabled 2016-06-05 02:04:31 +02:00
Daniel Gultsch
7a97da6d21 swap sending presence and csi 2016-06-04 22:42:12 +02:00
Daniel Gultsch
794353ad0c renamed last activity to last user interaction 2016-06-04 22:37:14 +02:00
Daniel Gultsch
71e9117176 opt-in to send last userinteraction in presence 2016-06-04 16:16:14 +02:00
Daniel Gultsch
6639d0f23b added section on backup to FAQ 2016-06-04 09:24:27 +02:00
Daniel Gultsch
becc3eb867 version bump to 1.12.9 + changelog 2016-06-03 23:57:18 +02:00
Daniel Gultsch
7398424f3b trim nick from bookmark before checking if it's empty 2016-06-03 19:24:11 +02:00
Daniel Gultsch
e26d842549 don't use a bookmarks name if it's empty 2016-06-03 18:43:45 +02:00
Andreas Straub
17c62b5991 Fix typo 2016-06-03 18:16:44 +02:00
Daniel Gultsch
161fdf7340 throw writeexecption in downloader if flush fails 2016-06-03 14:27:05 +02:00
Daniel Gultsch
e402348f9b disconnect account in background after deletion. fixes #1861 2016-06-03 14:18:43 +02:00
Daniel Gultsch
583aba1b44 print specific toast when download failed because of write error 2016-06-02 21:37:52 +02:00
Daniel Gultsch
594aab56db fixed regression that would not show clear devices 2016-06-02 20:46:01 +02:00
Daniel Gultsch
25211f13b3 make grace period configurable 2016-06-02 00:24:37 +02:00
Daniel Gultsch
e43a01159c deactive grace period when receiving screen on action 2016-06-01 21:51:46 +02:00
Daniel Gultsch
45cc33ca36 deactivate grace period when coming to foreground 2016-06-01 21:30:50 +02:00
Daniel Gultsch
20ba1add1e pulled translation from transifex 2016-06-01 11:38:57 +02:00
Daniel Gultsch
91732b89ea log background msgs not foreground msgs 2016-06-01 11:37:03 +02:00
Daniel Gultsch
add8e2cb74 don't replace \n\t\r 2016-06-01 09:04:08 +02:00
Daniel Gultsch
15316e6a7f only log inner stanza but display isCarbon 2016-06-01 09:03:21 +02:00
Daniel Gultsch
5c5d5cc4e3 don't show empty templates 2016-06-01 00:25:14 +02:00
Daniel Gultsch
24ea66c9fc display invite again menu item for offline members 2016-06-01 00:12:14 +02:00
Daniel Gultsch
ffba53777c check if session is optional 2016-05-31 23:09:45 +02:00
Daniel Gultsch
ea6a008b39 execute phone contact changes in singlethreadexecutor 2016-05-31 17:20:21 +02:00
Daniel Gultsch
1838023c88 log failure reason in http upload on wrong response code 2016-05-31 17:19:56 +02:00
Daniel Gultsch
b3337c4ad7 don't scroll to pos 0 when uuid wasn't found 2016-05-31 16:44:59 +02:00
Daniel Gultsch
b7c8ce1511 version bump to 1.12.8 2016-05-30 21:16:14 +02:00
Daniel Gultsch
6d0e5f4354 pulled translation from transifex 2016-05-30 21:16:04 +02:00
Daniel Gultsch
5b9ba79495 use whitespace as message seperator 2016-05-30 21:12:19 +02:00
Daniel Gultsch
9321ccc775 handle app links for conferences 2016-05-30 21:12:04 +02:00
Daniel Gultsch
8eb1640a26 remove unicode control chars before sending 2016-05-30 21:11:34 +02:00
Daniel Gultsch
be0fc59314 handle app links with @ in them 2016-05-30 13:06:42 +02:00
Daniel Gultsch
272cffe797 Revert "always notify by default in conferences"
This reverts commit e9494af098.

Now that new conferences are private by default this setting makes more sense
2016-05-29 22:55:01 +02:00
Daniel Gultsch
762820072a version bump to 1.12.7 2016-05-29 21:25:39 +02:00
Daniel Gultsch
ea18ceae4a avoid npe when sending omemo messages to group 2016-05-29 21:25:27 +02:00
Daniel Gultsch
71787bd2e1 version bump to 1.12.6 2016-05-29 20:54:41 +02:00
Daniel Gultsch
49cefd1c0c handle app links
invites in the form of https://conversations/i/localpart/domainpart
2016-05-29 20:44:58 +02:00
Daniel Gultsch
9afafe387a fix creation of conferences with 1 participant 2016-05-29 20:21:53 +02:00
Daniel Gultsch
107ab85a22 version bump to 1.12.5 + changelog 2016-05-29 13:09:15 +02:00
Daniel Gultsch
d89d7ade84 pulled translations from transifex 2016-05-29 13:00:02 +02:00
Daniel Gultsch
c3ec3ea70a don't merge messages over the char limit 2016-05-29 10:32:07 +02:00
Daniel Gultsch
2c55954ddd show in ui when text was shortened 2016-05-29 01:14:45 +02:00
Daniel Gultsch
aaf5233efe limit text size in message adapter to 2k and also limit text size in conversations adapter 2016-05-28 23:48:39 +02:00
Daniel Gultsch
422fd1847f only rendering first 5k chars of each message 2016-05-28 23:13:47 +02:00
Daniel Gultsch
ce0888b077 explicitly include version in issue template 2016-05-28 22:42:18 +02:00
Daniel Gultsch
fde27f447f count xmpp uris when disableing text selection 2016-05-28 17:01:05 +02:00
Daniel Gultsch
b3f50d1ad0 Merge branch 'master' of https://github.com/gjedeer/Conversations into gjedeer-master 2016-05-28 16:07:25 +02:00
Daniel Gultsch
bc326efd2c schedule first idle ping on service creation 2016-05-28 16:07:16 +02:00
Daniel Gultsch
bc36f1950f added idle ping in 10min intervals 2016-05-28 14:44:22 +02:00
Daniel Gultsch
ae7543bbfc put bug report jid in config. include package signature in report 2016-05-28 11:04:18 +02:00
Daniel Gultsch
06bef5de8d use EOT as message seperator 2016-05-28 11:03:29 +02:00
Daniel Gultsch
25f6651848 pulled translations from transifex 2016-05-27 20:07:39 +02:00
Daniel Gultsch
29bd1103c0 refactored toasts shown when adhoc creating mucs 2016-05-27 20:05:40 +02:00
Daniel Gultsch
a241ab66de use activity title 'choose participants' when doing that 2016-05-27 19:17:57 +02:00
Daniel Gultsch
f70fcc7bb8 use first letter to draw tiles for avatars
some users or conferences might have emojis in their names
2016-05-27 11:34:12 +02:00
Daniel Gultsch
44833c1499 don't push default muc conf twice 2016-05-27 10:35:00 +02:00
GDR!
82c3cbaf2a Add geo: link support in longer messages 2016-05-26 23:26:38 +02:00
Daniel Gultsch
21ebb35e44 add 'create conference' dialog 2016-05-26 22:53:55 +02:00
Daniel Gultsch
d9ff61ea2e show contact avatar in muc users unless that contact has its own avatar 2016-05-26 22:37:00 +02:00
Daniel Gultsch
841e718d6a make newly created conferences private by default 2016-05-26 12:39:31 +02:00
Daniel Gultsch
c4e82eb3f8 change hint in edit subject dialog 2016-05-26 12:39:04 +02:00
Daniel Gultsch
c06e2787c7 sending warning to receiving client if that client doesn't support omemo.
fixes #1873
2016-05-25 23:24:36 +02:00
Daniel Gultsch
83adbb6052 hide fingerprints in UI if encryption is disabled 2016-05-25 22:12:13 +02:00
Daniel Gultsch
5137837f6d only publish keys if omemo is enabled 2016-05-25 21:55:01 +02:00
Daniel Gultsch
c65c314801 only subscribe to omemo pep events if omemo is enabled 2016-05-25 21:54:46 +02:00
Daniel Gultsch
79796b0079 don't respond to otr messages in muc pms 2016-05-25 21:05:51 +02:00
Daniel Gultsch
b69ab65b12 show regitration failed try again later in UI 2016-05-24 13:26:30 +02:00
Daniel Gultsch
abbdf232c6 show hint in subject quick edit. only show subject as preset 2016-05-22 18:20:57 +02:00
Daniel Gultsch
d84cf4e6d1 pulled translations from transifex 2016-05-22 17:53:10 +02:00
Daniel Gultsch
e5b8302fd9 show first unread message on top after reinit 2016-05-22 17:52:27 +02:00
Daniel Gultsch
33218ec32a version bump to 1.12.4 + changelog 2016-05-21 13:58:15 +02:00
Daniel Gultsch
a8420c9ad0 disable stanza logging 2016-05-21 10:45:10 +02:00
Daniel Gultsch
277e3d59c8 update ui after affiliation changes 2016-05-21 09:25:37 +02:00
Daniel Gultsch
e1cf7b8cb6 refactore exceptionhandler to have one line file writer 2016-05-21 08:54:29 +02:00
Daniel Gultsch
9ce2cfa3d2 resetting fetch status error when mutual subscription is reestablished 2016-05-19 10:47:27 +02:00
Daniel Gultsch
8d595c1fc2 sync around individual calls instead of synchronizing entire object 2016-05-19 10:47:03 +02:00
Daniel Gultsch
ef27055434 show password dialog when account was magic created 2016-05-19 10:46:19 +02:00
Daniel Gultsch
3f65b0e985 access disco over caching mechanism instead of querying db 2016-05-19 10:44:16 +02:00
Daniel Gultsch
70497318dd remove unwanted 'use previous encryption' lookup 2016-05-19 10:42:57 +02:00
Daniel Gultsch
0eb8d4226e also save form elements in disco storage 2016-05-19 10:41:56 +02:00
Daniel Gultsch
627bf18f8c don't NPE on rare race condition while fetching MAM 2016-05-19 10:40:03 +02:00
Daniel Gultsch
afa3883089 synchronize around identity key generation 2016-05-19 10:39:47 +02:00
Daniel Gultsch
b478eca315 improved ordering of muc participants 2016-05-17 15:01:56 +02:00
Daniel Gultsch
61726f4994 refactored muc item parsing to also parse muc status messages 2016-05-17 14:25:58 +02:00
Daniel Gultsch
14952ba5e5 offer offline members to be invited again 2016-05-17 10:43:48 +02:00
Daniel Gultsch
fc5304c6fe change affiliation for in memory users that are currently not joined in a conference 2016-05-16 19:58:36 +02:00
Daniel Gultsch
8d0693ed6a keep conference members in memory and show them in conference details 2016-05-16 19:58:36 +02:00
Daniel Gultsch
d7c5264ad0 cap exponential backoff at 300s (10 attempts) 2016-05-16 19:58:24 +02:00
Daniel Gultsch
331cbf3696 cap messages after 256 lines in UI 2016-05-16 19:52:10 +02:00
Daniel Gultsch
6f1a4494eb use the same typo in both saving disco and reading disco 2016-05-15 12:35:51 +02:00
Daniel Gultsch
cf5ca27a06 escape HTML in OTR messages if other client is Pidgin 2016-05-15 12:35:31 +02:00
Daniel Gultsch
c9e9dc2ef2 include name in locations received in MUCs 2016-05-15 11:08:00 +02:00
Daniel Gultsch
a25912c32c log incoming iq requests 2016-05-15 09:55:06 +02:00
Daniel Gultsch
540f6f3d7a send caps hash in muc join
this prevents desktop clients from iq'ing use when they join
2016-05-15 09:54:49 +02:00
Daniel Gultsch
018f978a22 version bump to 1.12.3 + changelog 2016-05-13 12:01:07 +02:00
Daniel Gultsch
6a28b5a9fa don't show duplicate status message in contact details 2016-05-13 11:57:02 +02:00
Daniel Gultsch
e41a9483bd only default to omemo when all our devices support it 2016-05-13 11:47:29 +02:00
Daniel Gultsch
aced9d2697 do not process self presence
we don't want our own resource show up in the self contact
2016-05-13 11:20:27 +02:00
Daniel Gultsch
b756d61c45 show presence of other resources as template 2016-05-13 10:45:30 +02:00
Daniel Gultsch
e6ff1539b4 Update ISSUE_TEMPLATE.md 2016-05-13 09:02:10 +02:00
Daniel Gultsch
72f541140f create contributing guidelines 2016-05-13 08:59:43 +02:00
Daniel Gultsch
acad161344 Create issue template 2016-05-13 08:32:10 +02:00
Daniel Gultsch
b8c1bd2cba reset attempt count when reconnecting because of timeout 2016-05-12 21:57:07 +02:00
Daniel Gultsch
2014f388b1 interrupt XMPPConnection Thread
in some cases the the DNS query might take too long (even though we specified a timeout)
if that happens we need a secondary solution (besides killing the socket) to stop the thread
2016-05-12 21:54:46 +02:00
Daniel Gultsch
cbdb413613 prefer IPv4 DNS servers
some devices might have problems contacting the IPv6 DNS server while in sleep mode
2016-05-12 21:39:47 +02:00
Daniel Gultsch
f4369b29ae improve keyboard handling. fixes #1387
* start a new Conversations by pressing mod+space
* automatically start searching when pressing keys in StartConversationsActivity
* when hitting enter when number of search results == 1 open that conversation
2016-05-12 18:49:54 +02:00
Daniel Gultsch
7113e21a43 use 'phone' or 'tablet' as default resource 2016-05-12 18:47:41 +02:00
Daniel Gultsch
908aa19a36 make omemo default when all resources support it 2016-05-12 14:20:11 +02:00
Daniel Gultsch
09e20f6e01 check if pgpengine is still bound before using it 2016-05-12 11:30:53 +02:00
Daniel Gultsch
1bc92482e9 scroll to bottom after sending multi-line message 2016-05-12 10:39:04 +02:00
Daniel Gultsch
cc209afc51 stop processing PreKeyWhisperMessage if there is no PreKeyId
fixes #1832
2016-05-10 18:11:13 +02:00
Daniel Gultsch
8e3948e495 don’t let attempt count fall below zero 2016-05-10 17:48:09 +02:00
Daniel Gultsch
c37b5af2ca add lock domain and magic create domain to known hosts 2016-05-10 10:53:44 +02:00
Daniel Gultsch
e542dd3923 always show download button when link is encrypted
dont check for known mime type
2016-05-10 10:32:25 +02:00
Daniel Gultsch
549be9bb3d report host-account as account state in UI 2016-05-10 10:29:02 +02:00
Daniel Gultsch
27b245ac35 do not show last-seen metric in UI 2016-05-10 09:41:30 +02:00
Daniel Gultsch
488780d2ce fix logging wrong variable for failed resume 2016-05-08 21:53:45 +02:00
Daniel Gultsch
6f3b8f64d1 check for h attribute in 'failed' nonza 2016-05-08 21:45:18 +02:00
Daniel Gultsch
784df0c218 version bump to 1.12.2 + changelog 2016-05-07 11:35:12 +02:00
Daniel Gultsch
fb7525e0b9 catch all exceptions thrown by xml pull parser 2016-05-07 11:34:45 +02:00
Daniel Gultsch
76889b9c58 handle invalid base64 is SASl SCRAM response 2016-05-07 11:34:17 +02:00
Daniel Gultsch
e2d3bef739 Merge pull request #1829 from sebastianv89/patch-1
Remove copy of innerkey
2016-05-05 20:23:03 +02:00
Daniel Gultsch
a7cd05bd4e report bind failure as account state 2016-05-05 20:22:47 +02:00
Daniel Gultsch
0157039e87 log more information about HTTP’s max upload size 2016-05-05 19:34:44 +02:00
Sebastian
544e1dee65 Remove copy of innerkey
The line overwrites this.innerkey with the value that was already there.
2016-05-05 17:09:01 +02:00
Daniel Gultsch
6e0ec9b924 republish pgp signature when changing status 2016-05-05 13:17:04 +02:00
Daniel Gultsch
12704fa640 refactor captcha response handling to avoid network on main thread exception 2016-05-05 09:58:35 +02:00
Daniel Gultsch
8a81f85734 version bump to 1.12.1 + changelog 2016-05-04 22:24:07 +02:00
Daniel Gultsch
c27663c456 clear password field before setting new one 2016-05-04 18:23:36 +02:00
Daniel Gultsch
fb41a4ffaa fixed npe when calling changepassword activity directly 2016-05-04 18:22:17 +02:00
Daniel Gultsch
16eb1bfbd0 pulled translations from transifex 2016-05-04 13:19:07 +02:00
Daniel Gultsch
b334582eff Merge pull request #1827 from ka7/spelling_fix_no_translations
spelling fixes
2016-05-04 10:47:00 +02:00
klemens
7047d68165 spelling fixes 2016-05-04 10:29:29 +02:00
Daniel Gultsch
dee7fd3eab Merge pull request #1826 from sebastianv89/patch-1
Renaming of variable
2016-05-04 09:28:46 +02:00
Sebastian
cf374ec4ef Renaming of variable
Was probably just a copy/paste typo.
2016-05-03 23:35:57 +02:00
Daniel Gultsch
59f02f7766 Merge branch 'master' of github.com:siacs/Conversations 2016-05-03 22:17:16 +02:00
Daniel Gultsch
cef2eb58a7 fixed presence template dedup for 'online' status 2016-05-03 22:16:51 +02:00
Daniel Gultsch
fad8b702aa use app name in resource suggestions 2016-05-03 12:41:37 +02:00
Daniel Gultsch
cfa31beaf7 fixed spelling in readme 2016-05-03 09:39:30 +02:00
Daniel Gultsch
f444390617 update 'create account' faq entry 2016-05-02 14:39:58 +02:00
Daniel Gultsch
06a561743a ping all accounts at the same time 2016-05-02 14:31:30 +02:00
Daniel Gultsch
7674e01585 version bump to 1.12.0 + changelog 2016-05-02 11:05:53 +02:00
Daniel Gultsch
bf92ef6cd3 pulled translations from transifex 2016-05-02 11:05:31 +02:00
Daniel Gultsch
d23178acb9 show only username when registering account with magic create 2016-05-02 10:37:28 +02:00
Daniel Gultsch
98ecac0ffa removed unnecessary logging 2016-04-30 13:34:20 +02:00
Daniel Gultsch
936006173c properly cancel avatar tasks 2016-04-29 20:38:23 +02:00
Daniel Gultsch
d5608cb4f3 catch ActivityNotFoundException when requesting battery op 2016-04-29 13:58:37 +02:00
Daniel Gultsch
c7882b7225 port all android drop down list items to our own 2016-04-29 13:48:30 +02:00
Daniel Gultsch
6d9ca25915 catch rare NPE when determining max http size 2016-04-29 13:24:26 +02:00
Daniel Gultsch
252d015b71 synchronize around thumbnail cache to avoid loading images twice 2016-04-28 20:15:28 +02:00
Daniel Gultsch
1d2e2f71c2 cancel potential tasks when receiving image preview from cache 2016-04-28 20:14:53 +02:00
Daniel Gultsch
51753a1d39 cleaned up captcha dialog 2016-04-28 20:13:58 +02:00
Daniel Gultsch
5021b9a5dd don't request disco from self 2016-04-28 19:02:20 +02:00
Daniel Gultsch
29616d02a8 removed unused config variables 2016-04-27 16:43:02 +02:00
Daniel Gultsch
ebcb13c8eb made it possible to go back to welcome screen from edit account 2016-04-27 10:35:08 +02:00
Daniel Gultsch
e6b526230a renamed welcome header to untranslatable 'Start your Conversations' 2016-04-27 09:59:25 +02:00
Daniel Gultsch
9c3e910dc4 prevent user from accidentally changing password after using magic create 2016-04-26 23:23:48 +02:00
Daniel Gultsch
59652ecaf2 fixed table creation 2016-04-25 11:06:17 +02:00
Daniel Gultsch
6a677a172b Merge pull request #1821 from kriztan/patch-1
Update build.gradle
2016-04-24 11:26:01 +02:00
Christian S
94983ca3ed Update build.gradle
this seems be be never used and could be removed
2016-04-23 21:46:45 +02:00
Daniel Gultsch
a363e0a5d8 don't create templates for empty status messages 2016-04-23 15:10:35 +02:00
Daniel Gultsch
cd1fbf60ec add change prescence to manage account context menu 2016-04-23 12:33:56 +02:00
Daniel Gultsch
a9c1768107 show status messages in contact details 2016-04-23 12:19:00 +02:00
Daniel Gultsch
1901abd05f expert setting to manually change presence 2016-04-22 21:25:06 +02:00
Daniel Gultsch
195b745efc put welcome screen in scrollview 2016-04-22 00:17:08 +02:00
Daniel Gultsch
1a073ca454 added magic create welcome screen 2016-04-19 18:03:24 +02:00
Daniel Gultsch
bfe01c4322 version bump to 1.11.7 + changelog 2016-04-14 23:13:44 +02:00
Daniel Gultsch
e9494af098 always notify by default in conferences 2016-04-14 22:37:05 +02:00
Daniel Gultsch
eb63cdb9ad removed unnecessary call to stopSelf() after logging out 2016-04-14 21:45:36 +02:00
Daniel Gultsch
72aa10b536 add setting for quick sharing 2016-04-14 21:12:44 +02:00
Daniel Gultsch
39e717ed94 removed unused call to cancel events 2016-04-14 00:16:59 +02:00
Daniel Gultsch
c53c6cb6b6 create Config varibale to show the disable foreground service button 2016-04-13 18:00:12 +02:00
Daniel Gultsch
594e65bb2b hacky workaround to determine if uri points to private file on < lolipop 2016-04-13 11:14:36 +02:00
Daniel Gultsch
4332b0df44 return own jid as true counterpart on self messages in muc 2016-04-13 11:13:47 +02:00
Daniel Gultsch
3e654bea0e added share uri button to conference details 2016-04-12 18:30:02 +02:00
Daniel Gultsch
2a4db01709 reverse order in contact chooser 2016-04-12 18:29:41 +02:00
Daniel Gultsch
7223b5b274 minor code cleanup 2016-04-12 17:52:58 +02:00
Daniel Gultsch
7ff890e513 republish avatar if server offers non-persistent pep :-( 2016-04-11 22:20:32 +02:00
Daniel Gultsch
23a0beab43 version bump to 1.11.6 + changelog 2016-04-10 21:20:13 +02:00
Daniel Gultsch
77f4513862 pulled translations from transifex 2016-04-10 21:19:50 +02:00
Daniel Gultsch
677269606c add entries to gitignore 2016-04-10 00:31:25 +02:00
Daniel Gultsch
5786e75374 don't throw IO exception at end of stream 2016-04-10 00:19:53 +02:00
Daniel Gultsch
91b17c6925 fixed 'connecting…' button 2016-04-10 00:19:20 +02:00
Daniel Gultsch
607b7d1593 moved authentication into seperate method. force close socket before changing status 2016-04-10 00:18:14 +02:00
Daniel Gultsch
83fab06508 introduced setting to turn of notification led 2016-04-09 21:48:06 +02:00
Daniel Gultsch
4652541b61 update gradle and gradle plugin 2016-04-09 21:47:10 +02:00
Daniel Gultsch
65548ddccb use startdate as lower bound when querying archive with after=x 2016-04-09 12:31:08 +02:00
Daniel Gultsch
b99d70bfe7 don't show contact details when in conversations with self 2016-04-09 10:59:54 +02:00
Daniel Gultsch
2713fd50c8 use last received message id when querying archive 2016-04-09 10:29:34 +02:00
Daniel Gultsch
14b46c3ee7 transform nimbuzz workaround into a more general 'waitForDisco' condition 2016-04-09 08:53:58 +02:00
Daniel Gultsch
a8ebc5fafc add required disco#items query to timeout list 2016-04-08 20:20:37 +02:00
Daniel Gultsch
c22b384680 increase version code to fix nasty bug in 1.11.5 beta 2016-04-08 18:29:32 +02:00
Daniel Gultsch
db0301310b removed ernoexception in exchange for a regular exeption to prevent verify error on <5.0 2016-04-08 18:28:40 +02:00
Daniel Gultsch
7a84cfdfa2 version bump to 1.11.5 + changelog 2016-04-08 10:41:55 +02:00
Daniel Gultsch
c55f7645a4 pulled translations from transifex 2016-04-08 10:41:37 +02:00
Daniel Gultsch
0460702710 check file owner when attaching files or using them as avatar 2016-04-07 20:29:40 +02:00
Daniel Gultsch
290f0a123e prevent null pointer when checking http upload max size 2016-04-07 19:20:45 +02:00
Daniel Gultsch
275d6a858c tell people to build debug instead of release 2016-04-06 21:33:32 +02:00
Daniel Gultsch
b4ad2de2e5 version bump to 1.11.4 + changelog 2016-04-05 23:10:55 +02:00
Daniel Gultsch
ecaf75e5ec better detect broken pep
mark pep as broken when publishing bundle or device list failed
reset 'brokenness' when account is getting disabled
2016-04-05 13:31:03 +02:00
Daniel Gultsch
a968260b18 fixing travis 2016-04-04 21:25:44 +02:00
Daniel Gultsch
0385e3a8d6 switched around info and items query to avoid race condition 2016-04-04 20:35:40 +02:00
Daniel Gultsch
e94e06246b pulled translations from transifex 2016-04-04 20:21:00 +02:00
Daniel Gultsch
5787687997 removed unnecessary wait for disconnect 2016-04-04 20:07:09 +02:00
Daniel Gultsch
61997912fd made sure the disco#items query has returned before finalizing the bind 2016-04-04 20:06:07 +02:00
Daniel Gultsch
5eedce91f9 version bump to 1.11.3 and changelog 2016-04-02 18:09:07 +02:00
Daniel Gultsch
701742f550 don't ask for resource when server uses http upload v0.1 2016-04-02 18:07:38 +02:00
Daniel Gultsch
2549ce89b0 check max http file size when attaching files 2016-04-01 00:03:14 +02:00
Daniel Gultsch
74c496fe3e add methods to check max file size for http upload 2016-03-31 21:56:59 +02:00
Daniel Gultsch
e074104004 save otr fingerprint in message 2016-03-31 21:15:49 +02:00
Daniel Gultsch
867d0ef191 include form fields into caps hash calculation 2016-03-31 14:21:56 +02:00
Daniel Gultsch
8d98c52803 closed some cursors under error conditions 2016-03-31 13:55:46 +02:00
Daniel Gultsch
343a6b4e6b made setting aes keys in DownloadableFile more readable 2016-03-31 13:55:25 +02:00
Daniel Gultsch
d115f38361 Merge pull request #1784 from kriztan/patch-2
Update ShortcutBadger to version 1.1.4
2016-03-28 14:15:35 +02:00
Christian S
1d458e8ab3 Update ShortcutBadger to version 1.1.4 2016-03-27 20:17:51 +02:00
Daniel Gultsch
46be514b4d version bump to 1.11.2 and changelog 2016-03-23 19:24:54 +01:00
Daniel Gultsch
a9b66e3ea5 allow to delete attachments. fixes #1539 2016-03-23 19:23:22 +01:00
Daniel Gultsch
281cb65046 only add image files to media scanner 2016-03-23 12:20:09 +01:00
Daniel Gultsch
564113669e update build deps 2016-03-23 12:04:23 +01:00
Daniel Gultsch
0baa2dd03e Merge pull request #1780 from licaon-kter/patch-1
Typo `attempt`
2016-03-22 11:00:37 +01:00
licaon-kter
6ba90ec43c Typo attempt 2016-03-22 11:54:45 +02:00
Daniel Gultsch
135c8567a5 show room nick for /me command in sent muc messages. fixes #1773 2016-03-20 17:33:42 +01:00
Daniel Gultsch
ac09011690 be less strict when sharing EXTRA_TEXT intents 2016-03-20 17:25:16 +01:00
Daniel Gultsch
7df24407dc be more careful to avoid creating multiple connections 2016-03-20 17:24:41 +01:00
Daniel Gultsch
b51ce43d36 don't show v\omemo keys as such if not enabled 2016-03-20 17:24:15 +01:00
Daniel Gultsch
c4b1f6171d version bump to 1.11.1 and changelog 2016-03-16 18:12:36 +01:00
Daniel Gultsch
b17ca3543f made it possible to share text files 2016-03-16 18:09:19 +01:00
Daniel Gultsch
48be5af55f reworked sharewith activity to stay open during sharing
closing the activity prematuraly caused uri permissions to be revoked
2016-03-16 10:46:33 +01:00
Daniel Gultsch
323d31ba05 Merge pull request #1767 from fiaxh/path_file_accessible
Check if path for URI is accessible
2016-03-16 10:44:13 +01:00
fiaxh
eaddfa7fd1 Check if path for URI is accessible
The path extracted from the Cursor might not be accessible for Conversations. FileUtils accesses URI information through the ContentProvider, so this wouldn't be noticed.
Fixes sharing from open-keychain's TemporaryContentProvider
2016-03-15 11:42:13 +01:00
Daniel Gultsch
678bc7b4d4 removed requirement of external component for http upload 2016-03-14 12:16:36 +01:00
Daniel Gultsch
815c534da8 pulled translations from transifex 2016-03-13 17:43:43 +01:00
Daniel Gultsch
0af8ee341c simplified getUsers(max) code 2016-03-13 17:42:17 +01:00
Daniel Gultsch
1153e6120d added logging in case fragment wasn't attached 2016-03-13 17:41:38 +01:00
Daniel Gultsch
290f53f4a6 fixed recursive call instead of call to super in PublishProfileActivity 2016-03-13 17:39:13 +01:00
Daniel Gultsch
817d344521 log reason for bind failure 2016-03-11 09:01:40 +01:00
Daniel Gultsch
9548f43998 close cursor in caps db query 2016-03-11 09:01:27 +01:00
Daniel Gultsch
7eb736227e mention mod_smacks_offline and ejabberds settings in readme 2016-03-10 14:47:55 +01:00
Daniel Gultsch
1e75283250 version bump to 1.11.0 2016-03-07 11:06:41 +01:00
Daniel Gultsch
24aefa109c pulled translations from transifex 2016-03-06 21:35:59 +01:00
Daniel Gultsch
e6a9829dd2 don't show opt-out of battery optimization dialog when push is enabled 2016-03-06 15:53:49 +01:00
Daniel Gultsch
86fff5839a warn in conversations when account is disabled 2016-03-06 12:16:29 +01:00
Daniel Gultsch
8339ebf3dc version bump to 1.11.0-beta.3 2016-03-05 09:49:43 +01:00
Daniel Gultsch
d3542202b5 Merge branch 'Mess' of https://github.com/tarun018/Conversations into tarun018-Mess 2016-03-04 21:31:54 +01:00
Daniel Gultsch
e9b4a2a021 show host in file size checker 2016-03-04 21:30:34 +01:00
Daniel Gultsch
09d87965fb mark oob messages and always display download button 2016-03-04 20:09:21 +01:00
Daniel Gultsch
aa24a0f779 don't automatically crop avatar 2016-03-04 14:32:38 +01:00
Daniel Gultsch
89eea3636f add a few more know file extensions 2016-03-04 11:24:53 +01:00
Daniel Gultsch
07263370d9 allow to copy original url even while downloading. fixes #1743 2016-03-04 11:24:40 +01:00
Daniel Gultsch
cc67bfd8db version bump to 1.11.0-beta.2 2016-03-03 14:07:48 +01:00
Daniel Gultsch
bc5f64bffe moved avatarfetcher reset code to bind 2016-03-03 13:33:02 +01:00
Daniel Gultsch
4cb2d0ca93 avoid unnecessary disconnect. prevent NetworkOnMainThreadException 2016-03-03 13:31:59 +01:00
Daniel Gultsch
c9e4b332bf don't break with srcoll events on empty message lists 2016-03-03 11:14:59 +01:00
Daniel Gultsch
aaf64732b0 expert option to treat vibrate as silent mode for XA. fixes #1530 2016-03-01 19:00:18 +01:00
Daniel Gultsch
15a1873d97 removed unused config variable 2016-03-01 18:58:33 +01:00
Daniel Gultsch
fd246f7e5a properly persist accepted crypto targets 2016-03-01 12:22:20 +01:00
Daniel Gultsch
ab4d86dde7 version bump to 1.11.0-beta, changelog and updated readme 2016-03-01 11:44:51 +01:00
Daniel Gultsch
198dc2c6b4 let users confirm each member in a conference even if that contact is already trusted 2016-03-01 11:26:59 +01:00
Tarun
df7b399e04 Fix Issue #1634 : Shows XMPP URI as links.
Shows XMPP URI as links, other than Web URL's and Email Addresses. Also performs respective actions on clicking XMPP URI.
2016-02-29 23:35:50 +05:30
Daniel Gultsch
134c75ae01 use correct jid when leaving a conference. fixes #1732 2016-02-29 16:32:24 +01:00
Daniel Gultsch
9e0466d1e6 refactored omemo to take multiple recipients 2016-02-29 13:18:07 +01:00
Daniel Gultsch
199ae3a4d8 rename purge keys positive button to 'purge keys' 2016-02-28 23:10:50 +01:00
Daniel Gultsch
4ba41540fd made hashtable in roster store jids instead of strings 2016-02-28 20:45:50 +01:00
Daniel Gultsch
5cdfd0ec50 removed unneeded proguard files 2016-02-27 20:04:05 +01:00
Daniel Gultsch
24a9ac2908 always search offline contacts as well. fixes #1653 2016-02-27 15:41:34 +01:00
Daniel Gultsch
2c224d0f18 Merge branch 'master' of github.com:siacs/Conversations 2016-02-27 11:26:09 +01:00
Daniel Gultsch
3cf21e2d37 Merge pull request #1721 from fiaxh/export_logs_storage_permission
Request WRITE_EXTERNAL_STORAGE for ExportLogsPreference in >= M
2016-02-27 11:25:56 +01:00
Daniel Gultsch
60ab03afb1 changed single_account config into more simple lock_settings 2016-02-27 10:25:31 +01:00
Daniel Gultsch
c393e60891 version bump to 1.10.1 and changelog 2016-02-26 09:53:02 +01:00
Daniel Gultsch
7fd6a37e67 disallow message correction by default. fixes #1720 2016-02-26 09:48:58 +01:00
Daniel Gultsch
dc00a92499 execute pending mam queries every time we come online 2016-02-26 09:46:25 +01:00
Daniel Gultsch
5d3ee60ca4 hide add account icons when single_account is set to true 2016-02-24 17:12:29 +01:00
Daniel Gultsch
bbede8bbeb optionally lock conference domains as well and hide known domains in ui 2016-02-24 16:53:19 +01:00
fiaxh
e1a2f248af Request WRITE_EXTERNAL_STORAGE for ExportLogsPreference in >= M 2016-02-24 16:35:26 +01:00
fiaxh
a88c2d48c0 No possibility of multiple invocation of log export 2016-02-24 15:10:41 +01:00
Daniel Gultsch
d1a456f3e3 made hard coded choice for encryptions more flexible and disable parsing 2016-02-24 14:47:49 +01:00
Daniel Gultsch
ddafa65849 Merge pull request #1715 from fiaxh/gpg_decryption_failed
PGP Retry decryption from message menu
2016-02-24 09:23:30 +01:00
Daniel Gultsch
17b1fcc3ea set noMessagesLeftOnServer before conference configuration fetch 2016-02-23 16:15:55 +01:00
Daniel Gultsch
34f2a63190 update notification after message correction 2016-02-23 16:15:23 +01:00
Daniel Gultsch
0298f0181e reset pending subscription request when receiving roster update 2016-02-23 16:14:55 +01:00
fiaxh
894b5892a9 Retry decryption from message menu 2016-02-23 16:05:42 +01:00
Daniel Gultsch
20eebe638b Revert "disable predexing on travis"
This reverts commit ad063d00cc.
2016-02-23 14:33:03 +01:00
Daniel Gultsch
ad063d00cc disable predexing on travis 2016-02-23 14:25:13 +01:00
Daniel Gultsch
beb216c300 made presences object final in contact 2016-02-23 14:25:01 +01:00
Daniel Gultsch
7f45e210af fixed typo in travis.yml 2016-02-23 13:54:24 +01:00
Daniel Gultsch
f1c947f0d6 fixed formating in travis config 2016-02-23 11:20:44 +01:00
Daniel Gultsch
9ae997cab8 added remark that users don't need their own app server 2016-02-23 10:15:07 +01:00
Daniel Gultsch
4a9753bebc tell travis to build free version 2016-02-23 09:29:18 +01:00
Daniel Gultsch
5eb2d9af83 update build instructions in readme 2016-02-23 09:07:47 +01:00
Daniel Gultsch
689ded1607 properly trigger show load more messages in mucs 2016-02-22 20:28:58 +01:00
Daniel Gultsch
a0d0ed34ae turned muc errors into enum. added error codes for service shutdown 2016-02-22 20:19:58 +01:00
Daniel Gultsch
c20d8ac69e version bump to 1.10.0 2016-02-21 23:03:30 +01:00
Daniel Gultsch
d2cfac222e show load more messages when auto loading is disabled and messages are still left on server 2016-02-21 17:32:46 +01:00
Daniel Gultsch
b00c561f81 check for uuid change when decrypting pgp messages 2016-02-21 11:43:03 +01:00
Daniel Gultsch
ed740b4868 some mucs may grant voice to visitors in unmoderated rooms 2016-02-21 11:42:41 +01:00
Daniel Gultsch
43b466704a pulled translations from transifex 2016-02-20 10:25:23 +01:00
Daniel Gultsch
3bde4dbedb change uuid when replacing messages 2016-02-20 00:01:39 +01:00
Daniel Gultsch
e6f8b7d9fa decrypt pgp message corrections 2016-02-19 21:02:33 +01:00
Daniel Gultsch
a2cb009f4c skip avatar ui when pep is not available. fixes #1706 2016-02-19 20:54:53 +01:00
Daniel Gultsch
df992d2566 don't reset whisper on reInit. fixes #1637 2016-02-19 20:54:43 +01:00
Daniel Gultsch
ad60bc002c pulled translations from transifex 2016-02-19 11:14:16 +01:00
Daniel Gultsch
49a3f6f281 never parse show in presences as offline 2016-02-19 11:09:28 +01:00
Daniel Gultsch
ac687d6bbd don't log start reason 2016-02-17 16:52:57 +01:00
Daniel Gultsch
59978e157c only offer message correction for the very last message 2016-02-17 16:51:36 +01:00
Daniel Gultsch
3626e4b3a0 fixed regression that caused messages in muc not being send 2016-02-17 16:50:48 +01:00
Daniel Gultsch
c2fbdbde83 log reason why otr message won't be parsed 2016-02-16 14:22:47 +01:00
Daniel Gultsch
86b1865eec fixed regression that caused ui to redraw a lot 2016-02-16 14:22:21 +01:00
Daniel Gultsch
726393f8da version bump to 1.10.0-beta and changelog 2016-02-16 12:59:54 +01:00
Daniel Gultsch
349dd8291d made clear that archiving preferences are server side 2016-02-16 12:52:31 +01:00
Daniel Gultsch
c39008983e Merge pull request #1700 from petmue/master
Fixed some typos in the README.md
2016-02-16 12:27:29 +01:00
petmue
a27cbfbf56 Fixed some typos. 2016-02-16 12:18:43 +01:00
Daniel Gultsch
7d63b06d84 Update README.md 2016-02-16 10:36:40 +01:00
Daniel Gultsch
d06013fbaf updated XEP list 2016-02-16 10:04:03 +01:00
Daniel Gultsch
a5e40672c1 added gcm values file to gitignore 2016-02-16 09:58:26 +01:00
Daniel Gultsch
a9b957e8a2 added setting to opt-out of message correction. renamed preferences and options to settings 2016-02-16 09:57:59 +01:00
Daniel Gultsch
0ca4a33bfb added some OTR logging 2016-02-16 09:15:41 +01:00
Daniel Gultsch
c0b3a3ff0c basic support for XEP-0308: Last Message Correction. fixes #864 2016-02-15 23:15:04 +01:00
Daniel Gultsch
335058b78b removed unnecessary conditions when sending read marker 2016-02-15 23:09:42 +01:00
Daniel Gultsch
c4b1df1bf3 add missing type='submit' attribute to enable push form 2016-02-15 22:12:39 +01:00
Daniel Gultsch
c3f0503a91 pulled translations from transifex 2016-02-15 12:35:35 +01:00
Daniel Gultsch
8ccb2005b3 only show load more messages button when mam is available
also update ui after that button has been pressed. fixes #1695
2016-02-14 23:53:17 +01:00
Daniel Gultsch
356199978e fixed server info push not showing up when unavailable 2016-02-14 18:19:11 +01:00
Daniel Gultsch
92a6e956fd be more carefull when checking push availability 2016-02-14 15:36:37 +01:00
Daniel Gultsch
300326fba3 deleted invalid gcm strings 2016-02-14 14:14:53 +01:00
Daniel Gultsch
251f2479c2 optional mode to close tcp connection when going into background
acts only when push is available. disable all non-push accounts to test properly
2016-02-14 13:20:23 +01:00
Daniel Gultsch
6f9f871928 send push enable to server. simplified logging 2016-02-13 14:20:07 +01:00
Daniel Gultsch
c7a14092a8 fixed compile bug in free version 2016-02-13 00:03:57 +01:00
Daniel Gultsch
6217e33a87 removed gcm plugin from gradle. fixes #1693 2016-02-12 23:38:30 +01:00
Daniel Gultsch
c430848ade push gcm token on bind instead of every connect 2016-02-12 23:37:42 +01:00
Daniel Gultsch
bac249c8dd add play services to travis config 2016-02-12 12:06:35 +01:00
Daniel Gultsch
32da65f910 client side support for XEP-0357: Push Notifications 2016-02-12 11:39:27 +01:00
Daniel Gultsch
93dad9b737 pulled translations from transifex 2016-02-11 22:45:40 +01:00
Daniel Gultsch
d58d822215 version bump to 1.9.4 + changelog 2016-02-11 17:13:17 +01:00
Daniel Gultsch
f37098a54f catch all axolotl parse exception at once. fixes #1692 2016-02-11 12:26:43 +01:00
Daniel Gultsch
1bb38e25f2 send muc messages after join 2016-02-10 09:53:48 +01:00
Daniel Gultsch
f16690ae1f allow user to set MAM preferences 2016-02-09 13:01:17 +01:00
Daniel Gultsch
91ec4839ac prepend instead off append mam messages to conversations when going in reverse 2016-02-04 16:40:18 +01:00
Daniel Gultsch
28733e052f fixed performance regression in on scroll listener 2016-02-04 16:29:17 +01:00
Daniel Gultsch
4fdb0d92fe prevent previoulsly cleared messages from reloading. fixes #1110 2016-02-04 14:39:16 +01:00
Daniel Gultsch
f88b8c703e add more fault tolerant checks for messages left on server 2016-02-04 11:55:42 +01:00
Daniel Gultsch
17791a703e removed unecessary logging when muc tiles update 2016-02-04 10:27:38 +01:00
Daniel Gultsch
7dd9545ea3 use TLSv1.2 as SSL context on supported plattforms 2016-02-03 18:17:16 +01:00
Daniel Gultsch
1d572c61d0 cache server caps 2016-02-03 17:19:05 +01:00
Daniel Gultsch
0911669b07 count all messages in a query 2016-02-03 16:04:21 +01:00
Daniel Gultsch
1274b0ef39 Revert "get rid of broken totalMessageCount for mam queries"
This reverts commit 58c6f9bfb2.
2016-02-03 10:40:44 +01:00
Daniel Gultsch
f0798216d5 refactored disco cache. avoid making duplicate call. check hash 2016-02-03 10:40:02 +01:00
Daniel Gultsch
4a1a59f0c8 Merge branch 'disco-caps' of https://github.com/singpolyma/Conversations into singpolyma-disco-caps 2016-02-02 18:19:26 +01:00
Daniel Gultsch
01bad12708 fixed 'unencrypted' not showing up for conferences when encryption is forced 2016-02-02 18:15:57 +01:00
Daniel Gultsch
58c6f9bfb2 get rid of broken totalMessageCount for mam queries 2016-02-02 15:39:46 +01:00
Daniel Gultsch
fab0a45955 re-read common name from certificates on startup 2016-02-02 13:43:20 +01:00
Daniel Gultsch
ba9ba8ffe2 avoid npe when accessing the pgp connection service 2016-02-02 11:21:29 +01:00
Daniel Gultsch
f30df7a535 catch a few NPE when parsing invalid pep nodes 2016-02-02 11:21:07 +01:00
Stephen Paul Weber
ae84ff2f0c Do disco for caps hashes we have never seen
Then cache it
2016-01-24 17:46:08 -05:00
Stephen Paul Weber
000f59d614 Fetch cached caps result on new presence 2016-01-24 17:46:08 -05:00
Stephen Paul Weber
bf5b2f73f5 Use a Presence class for presence information
Only has status for now, but doing it so I can add disco to it
2016-01-24 17:46:08 -05:00
Stephen Paul Weber
ad36a4ba89 Persisitence and loading for ServiceDiscoveryResult 2016-01-23 10:53:56 -05:00
Stephen Paul Weber
56f8fff935 Implement toJSON on ServiceDiscoveryResult 2016-01-23 10:52:45 -05:00
Stephen Paul Weber
1e335d527b Generate capHash from any discovery result 2016-01-23 10:52:44 -05:00
Stephen Paul Weber
fccce229c6 Factor out a representation of XEP-0030 results
And the parser from Element to this representation.
2016-01-23 10:52:40 -05:00
624 changed files with 24815 additions and 7301 deletions

11
.github/CONTRIBUTING.md vendored Normal file
View file

@ -0,0 +1,11 @@
### Reporting Bugs and getting help
The issue tracker on Github is for bug reports only. It is not a general support forum.
**A bug is everything you can reproduce. ie *if I do this that happens but something else should happen instead*.**
Please search the issue tracker (including closed issues). Your bug might be a server bug that has already been addressed.
Please fill in the template when creating new issues and provide information about your device and your server. The [adb logcat](https://wiki.cyanogenmod.org/w/Doc:_debugging_with_logcat) can be omitted if you can reproduce the bug under every condition. (ie *click button X and the application crashes*) But the adb logcat is required if the bug happens only under certain conditions or involves network problem (Server not found, messages not arriving)
**If you are seeking help or are unable to reproduce the exact problem please join our MUC at conversations@conference.siacs.eu**

30
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,30 @@
#### General information
* **Version:** eg 1.12.2
* **Device:** eg Google Nexus 5
* **Android Version:** eg Android 6.0 Stock or Android 5.1 Cyanogenmod
* **Server name:** eg conversations.im, jabber.at or self hosted
* **Server software:** ejabberd 16.04 or prosody 0.10 (if known)
* **Installed server modules:** eg Stream Managment, CSI, MAM
* **Conversations source:** eg PlayStore, PlayStore Beta Channel, F-Droid, self build (latest HEAD)
#### Steps to reproduce
1. …
2. …
#### Expected result
What is the expected output? What do you see instead?
#### Debug output
Please post the output of adb logcat. The log should begin with the start of Conversations and include all the
steps it takes to reproduce the problem.
````
adb logcat -s conversations
````

4
.gitignore vendored
View file

@ -2,9 +2,13 @@
*.swp
.settings
src/playstore/res/values/gcm.xml
# https://github.com/github/gitignore/blob/master/Gradle.gitignore
.gradle/
build/
captures/
gradle.properties
# Ignore Gradle GUI config
gradle-app.setting

View file

@ -1,13 +1,16 @@
language: android
jdk:
- oraclejdk8
android:
components:
- platform-tools
- tools
- build-tools-23.0.2
- build-tools-23.0.1
- build-tools-23.0.0
- build-tools-22.0.1
- build-tools-21.1.2
- build-tools-19.1.0
- android-23
- build-tools-25.0.2
- android-25
- extra-android-m2repository
- extra-google-m2repository
- extra-google-google_play_services
licenses:
- '.+'
script:
- ./gradlew assembleFreeRelease

View file

@ -1,5 +1,195 @@
###Changelog
####Version 1.15.5
* show nick as bold text when mentioned in conference
* bug fixes
####Version 1.15.4
* bug fixes
####Version 1.15.3
* show offline contacts in MUC as grayed-out
* don't transcode gifs. add overlay indication to gifs
* bug fixes
####Version 1.15.2
* bug fixes
####Version 1.15.1
* support for POSH (RFC7711)
* support for quoting messages (via select text)
* verified messages show shield icon. unverified messages show lock
####Version 1.15.0
* New [Blind Trust Before Verification](https://gultsch.de/trust.html) mode
* Easily share Barcode and XMPP uri from Account details
* Automatically deactivate own devices after 7 day of inactivity
* Improvements fo doze/push mode
* bug fixes
####Version 1.14.9
* warn in account details when data saver is enabled
* automatically enable foreground service after detecting frequent restarts
* bug fixes
####Version 1.14.8
* bug fixes
####Version 1.14.7
* error message accessible via context menu for failed messages
* don't include pgp signature in anonymous mucs
* bug fixes
####Version 1.14.6
* make error notification dismissable
* bug fixes
####Version 1.14.5
* expert setting to delete OMEMO identities
* bug fixes
####Version 1.14.4
* bug fixes
####Version 1.14.3
* XEP-0377: Spam Reporting
* fix rare start up crashes
####Version 1.14.2
* support ANONYMOUS SASL
* bug fixes
####Version 1.14.1
* Press lock icon to see why OMEMO is deactivated
* bug fixes
####Version 1.14.0
* Improvments for N
* Quick Reply to Notifications on N
* Don't download avatars and files when data saver is on
* bug fixes
####Version 1.13.9
* bug fixes
####Version 1.13.8
* show identities instead of resources in selection dialog
* allow TLS direct connect when port is set to 5223
* bug fixes
####Version 1.13.7
* bug fixes
####Version 1.13.6
* thumbnails for videos
* bug fixes
####Version 1.13.5
* bug fixes
####Version 1.13.4
* support jingle ft:4
* show contact as DND if one resource is
* bug fixes
####Version 1.13.3
* bug fixes
####Version 1.13.2
* new PGP decryption logic
* bug fixes
####Version 1.13.1
* changed some colors in dark theme
* fixed fall-back message for OMEMO
####Version 1.13.0
* configurable dark theme
* opt-in to share Last User Interaction
####Version 1.12.9
* make grace period configurable
####Version 1.12.8
* more bug fixes :-(
####Version 1.12.7
* bug fixes
####Version 1.12.6
* bug fixes
####Version 1.12.5
* new create conference dialog
* show first unread message on top
* show geo uri as links
* circumvent long message DOS
####Version 1.12.4
* show offline members in conference (needs server support)
* various bug fixes
####Version 1.12.3
* make omemo default when all resources support it
* show presence of other resources as template
* start typing in StartConversationsActivity to search
* various bug fixes and improvements
####Version 1.12.2
* fixed pgp presence signing
####Version 1.12.1
* small bug fixes
####Version 1.12.0
* new welcome screen that makes it easier to register account
* expert setting to modify presence
####Version 1.11.7
* Share xmpp uri from conference details
* add setting to allow quick sharing
* various bug fixes
####Version 1.11.6
* added preference to disable notification light
* various bug fixes
####Version 1.11.5
* check file ownership to not accidentally share private files
####Version 1.11.4
* fixed a bug where contacts are shown as offline
* improved broken PEP detection
####Version 1.11.3
* check maximum file size when using HTTP Upload
* properly calculate caps hash
####Version 1.11.2
* only add image files to media scanner
* allow to delete files
* various bug fixes
####Version 1.11.1
* fixed some bugs when sharing files with Conversations
####Version 1.11.0
* OMEMO encrypted conferences
####Version 1.10.1
* made message correction opt-in
* various bug fixes
####Version 1.10.0
* Support for XEP-0357: Push Notifications
* Support for XEP-0308: Last Message Correction
* introduced build flavors to make dependence on play-services optional
####Version 1.9.4
* prevent cleared Conversations from reloading history with MAM
* various MAM fixes
####Version 1.9.3
* expert setting that enables host and port configuration
* expert setting opt-out of bookmark autojoin handling
@ -123,7 +313,7 @@
####Version 1.4.0
* send button turns into quick action button to offer faster access to take photo, send location or record audio
* visually seperate merged messages
* visually separate merged messages
* faster reconnects of failed accounts after network switches
* r/o vcard avatars for contacts
* various bug fixes
@ -216,7 +406,7 @@
* Download HTTP images
* Show avatars in MUC tiles
* Disabled SSLv3
* Performance improvments
* Performance improvements
* bug fixes
####Version 0.7.3
@ -265,7 +455,7 @@
####Version 0.4
* OTR file encryption
* keep OTR messages and files on device until both parties or online at the same time
* XEP-0333. Mark wether the other party has read your messages
* XEP-0333. Mark whether the other party has read your messages
* Delayed messages are now tagged properly
* Share images from the Gallery
* Infinit history scrolling

128
README.md
View file

@ -16,7 +16,7 @@ Conversations: the very last word in instant messaging
## Features
* End-to-end encryption with [OMEMO](http://conversations.im/omemo/), [OTR](https://otr.cypherpunks.ca/), or [OpenPGP](http://www.openpgp.org/about_openpgp/)
* End-to-end encryption with [OMEMO](http://conversations.im/omemo/), [OTR](https://otr.cypherpunks.ca/), or [OpenPGP](http://openpgp.org/about/)
* Send and receive images as well as other kind of files
* Share your location via an external [plug-in](https://play.google.com/store/apps/details?id=eu.siacs.conversations.sharelocation&referrer=utm_source%3Dgithub)
* Indication when your contact has read your message
@ -56,9 +56,8 @@ run your own XMPP server for you and your friends. These XEP's are:
* [XEP-0352: Client State Indication](http://xmpp.org/extensions/xep-0352.html) lets the server know whether or not
Conversations is in the background. Allows the server to save bandwidth by
withholding unimportant packages.
* [XEP-0363: HTTP File Upload](http://xmpp.org/extensions/xep-0363.html) allows you to share files in conferences and with offline
contacts. Requires an [additional component](https://github.com/siacs/HttpUploadComponent)
on your server. Alternatively, an [Ejabberd contrib-module](https://github.com/processone/ejabberd-contrib/tree/master/mod_http_upload) and a [Prosody module](http://modules.prosody.im/mod_http_upload.html) are available.
* [XEP-0363: HTTP File Upload](http://xmpp.org/extensions/xep-0363.html) allows you to share files in conferences
and with offline contacts.
## Team
@ -119,20 +118,27 @@ My Bitcoin Address is: `1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu`
[![Flattr this!](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=inputmice&url=http%3A%2F%2Fconversations.siacs.eu&title=Conversations&tags=github&category=software)
#### How do I create an account?
XMPP, like email, is a federated protocol, which means that there is not one company you can create an *official XMPP account* with. Instead there are hundreds, or even thousands, of providers out there. One of those providers is our very own [conversations.im](https://account.conversations.im). If you 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.
XMPP, like email, is a federated protocol which means that there is not one
company you can create an 'official XMPP account' with. Instead there are
hundreds, or even thousands, of provider out there. To find one use a web search
engine of your choice. Or maybe your university has one. Or you can run your
own. Or ask a friend to run one. Once you've found one, you can use
Conversations to create an account. Just select 'register new account on server'
within the create account dialog.
##### Domain hosting
Using your own domain not only gives you a more recognizable Jabber ID, it also gives you the flexibility to migrate your account between different XMPP providers. This is a good compromise between the responsibilities of having to operate your own server and the downsides of being dependent on a single provider.
Learn more about [conversations.im Jabber/XMPP domain hosting](https://account.conversations.im/domain/).
##### Running your own
If you already have a server somewhere and are willing and able to put the necessary work in, one alternative-in the spirit of federation-is to run your own. We recommend either [Prosody](https://prosody.im/) or [ejabberd](https://www.ejabberd.im/). Both of which have their own strengths. Ejabberd is slightly more mature nowadays but Prosody is arguably easier to set up.
For Prosody you need a couple of so called [community modules](https://modules.prosody.im/) most of which are maintained by the same people that develop Prosody.
If you pick ejabberd make sure you use the latest version. Linux Distributions might bundle some very old versions of it.
#### Where can I set up a custom hostname / port
Conversations will automatically look up the SRV records for your domain name
which can point to any hostname port combination. If your server doesnt provide
those please contact your admin and have them read
[this](http://prosody.im/doc/dns#srv_records)
[this](http://prosody.im/doc/dns#srv_records). If your server operator is unwilling
to fix this you can enable advanced server settings in the expert settings of
Conversations.
#### I get 'Incompatible Server'
@ -146,6 +152,15 @@ On rare occasions this error message might also be caused by a server not provid
a login (SASL) mechanism that Conversations is able to handle. Conversations supports
SCRAM-SHA1, PLAIN, EXTERNAL (client certs) and DIGEST-MD5.
#### How do XEP-0357: Push Notifications work?
You need to be running the Play Store version of Conversations and your server needs to support push notifications.¹ Because *Google Cloud Notifications (GCM)* are tied with an API key to a specific app your server can not initiate the push message directly. Instead your server will send the push notification to the Conversations App server (operated by us) which then acts as a proxy and initiates the push message for you. The push message sent from our App server through GCM doesnt contain any personal information. It is just an empty message which will wake up your device and tell Conversations to reconnect to your server. The information send from your server to our App server depends on the configuration of your server but can be limited to your account name. (In any case the Conversations App server won't redirect any information through GCM even if your server sends this information.)
In summary Google will never get hold of any personal information besides that *something* happened. (Which doesnt even have to be a message but can be some automated event as well.) We - as the operator of the App server - will just get hold of your account name (without being able to tie this to your specific device).
If you dont want this simply pick a server which does not offer Push Notifications or build Conversations yourself without support for push notifications. (This is available via a gradle build flavor.) Non-play store source of Conversations like the Amazon App store will also offer a version without push notifications. Conversations will just work as before and maintain its own TCP connection in the background.
¹ Your server only needs to support the server side of [XEP-0357: Push Notifications](http://xmpp.org/extensions/xep-0357.html). If you use the Play Store version you do **not** need to run your own app server. The server modules are called *mod_cloud_notify* on Prosody and *mod_push* on ejabberd.
#### Conversations doesn't work for me. Where can I get help?
You can join our conference room on `conversations@conference.siacs.eu`.
@ -186,6 +201,12 @@ connection again. When the client fails to do so because the network
connectivity is out for longer than that all messages sent to that client will
be returned to the sender resulting in a delivery failed.
Instead of returning a message to the sender both ejabberd and prosody have the
ability to store messages in offline storage when the disconnecting client is
the only client. In prosody this is available via an extra module called
```mod_smacks_offline```. In ejabberd this is available via some configuration
settings.
Other less common reasons are that the message you sent didn't meet some
criteria enforced by the server (too large, too many). Another reason could be
that the recipient is offline and the server doesn't provide offline storage.
@ -226,6 +247,11 @@ Making these status and priority optional isn't a solution either because
Conversations is trying to get rid of old behaviours and set an example for
other clients.
#### How do I backup / move Conversations to a new device?
On the one hand Conversations supports Message Archive Management to keep a server side history of your messages so when migrating to a new device that device can display your entire history. However that does not work if you enable OMEMO due to its forward secrecy. (Read [The State of Mobile XMPP in 2016](https://gultsch.de/xmpp_2016.html) especially the section on encryption.)
If you migrate to a new device and would still like to keep your history please use a third party backup tool like [oandbackup](https://github.com/jensstein/oandbackup) or ```adb backup``` from your computer. It is important that your deactivate your account before backup and activate it only after a successful restore. Otherwise OMEMO might not work afterwards.
#### Conversations is missing a certain feature
I'm open for new feature suggestions. You can use the [issue tracker][issues] on
@ -250,7 +276,9 @@ I am available for hire. Contact me via XMPP: `inputmice@siacs.eu`
#### Why are there three end-to-end encryption methods and which one should I choose?
In most cases OTR should be the encryption method of choice. It works out of the box with most contacts as long as they are online. However, openPGP can, in some cases, (message carbons to multiple clients) be more flexible. Unlike OTR, OMEMO works even when a contact is offline, and works with multiple devices. It also allows asynchronous file-transfer when the server has [HTTP File Upload](http://xmpp.org/extensions/xep-0363.html). However, OMEMO is not as widely supported as OTR and is currently implemented only by Conversations. OMEMO should be preffered over OTR for contacts who use Conversations.
* OTR is a legacy encryption method. It works out of the box with most contacts as long as they are online.
* OMEMO works even when a contact is offline, and works with multiple devices. It also allows asynchronous file-transfer when the server has [HTTP File Upload](http://xmpp.org/extensions/xep-0363.html). However, OMEMO is not as widely supported as OTR and is currently implemented only by Conversations and Gajim. OMEMO should be preferred over OTR for contacts who use Conversations.
* OpenPGP (XEP-0027) is a very old encryption method that has some advantages over OTR but should only be used by experts who know what they are doing.
#### How do I use OpenPGP
@ -269,14 +297,36 @@ To use OpenPGP you have to install the open source app
[OpenKeychain](http://www.openkeychain.org) and then long press on the account in
manage accounts and choose renew PGP announcement from the contextual menu.
#### OMEMO is grayed out. What do I do?
OMEMO has two requirements: Your server and the server of your contact need to support PEP. Both of you can verify that individually by opening your account details and selecting ```Server info``` from the menu. The appearing table should list PEP as available. The second requirement is mutual presence subscription. You can verify that by opening the contact details and see if both check boxes *Send presence updates* and *Receive presence updates* are checked.
#### How does the encryption for conferences work?
For conferences the only supported encryption method is OpenPGP (OTR does not
work with multiple participants). Every participant has to announce their
OpenPGP key (see answer above). If you would like to send encrypted messages to
a conference you have to make sure that you have every participant's public key
in your OpenKeychain. Right now there is no check in Conversations to ensure
that. You have to take care of that yourself. Go to the conference details and
For conferences only OMEMO and OpenPGP are supported as encryption method. (OTR
does not work with multiple participants).
##### OMEMO
OMEMO encryption works only in private (members only) conferences that are non-anonymous.
You need to have presence subscription with every member of the conference.
You can verify that by going into the conference details, long press every member and start
a conversation with them. (Or select 'contact details' if they are already in your contact
list)
The owner of a conference can make a public conference private by going into the conference
details and hit the settings button (the one with the gears) and select both *private* and
*members only*.
If OMEMO is grayed out long pressing the lock icon will reveal some quick hints on why OMEMO
is disabled.
##### OpenPGP
Every participant has to announce their OpenPGP key (see answer above).
If you would like to send encrypted messages to a conference you have to make
sure that you have every participant's public key in your OpenKeychain.
Right now there is no check in Conversations to ensure that.
You have to take care of that yourself. Go to the conference details and
touch every key id (The hexadecimal number below a contact). This will send you
to OpenKeychain which will assist you on adding the key. This works best in
very small conferences with contacts you are already using OpenPGP with. This
@ -284,6 +334,23 @@ feature is regarded experimental. Conversations is the only client that uses
XEP-0027 with conferences. (The XEP neither specifically allows nor disallows
this.)
#### Why is Conversations not end-to-end encrypted by default
We briefly had OMEMO as the default E2EE but it turned out to be a usability nightmare and thus we reverted that. You can find more information in [the commit message](https://github.com/siacs/Conversations/commit/035d0c79572d5981c53d1bff7f30b484c6542f17) of that change.
Quick reminder that Conversations **always** uses TLS to connect to your server. It 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 recommend that you use [Gajim](https://gajim.org). You need to install the plugins `OMEMO`, `HTTP Upload` and `URL image preview` to get the best compatibility with Conversations. Plugins can be installed from within the app.
####iOS
Unfortunately we 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
<a name="beta"></a>
@ -296,16 +363,18 @@ to sign up for the beta test.
#### How do I build Conversations
Make sure to have ANDROID_HOME point to your Android SDK
Make sure to have ANDROID_HOME point to your Android SDK. Use the Android SDK Manager to install missing dependencies.
git clone https://github.com/siacs/Conversations.git
cd Conversations
./gradlew build
./gradlew assembleFreeDebug
There are two build flavors available. *free* and *playstore*. Unless you know what you are doing you only need *free*.
[![Build Status](https://travis-ci.org/siacs/Conversations.svg?branch=development)](https://travis-ci.org/siacs/Conversations)
### How do I update/add external libraries?
#### How do I update/add external libraries?
If the library you want to update is in Maven Central or JCenter (or has its own
Maven repo), add it or update its version in `build.gradle`. If the library is
@ -328,16 +397,27 @@ To add a new dependency to the `libs/` directory (replacing "name", "branch" and
If something goes wrong Conversations usually exposes very little information in
the UI (other than the fact that something didn't work). However with adb
(android debug bridge) you squeeze some more information out of Conversations.
(android debug bridge) you can squeeze some more information out of Conversations.
These information are especially useful if you are experiencing trouble with
your connection or with file transfer.
To use adb you have to connect your mobile phone to your computer with an USB cable
and install `adb`. Most Linux systems have prebuilt packages for that tool. On
Debian/Ubuntu for example it is called `android-tools-adb`.
Furthermore you might have to enable 'USB debugging' in the Developer options of your
phone. After that you can just execute the following on your computer:
adb -d logcat -v time -s conversations
If need be there are also some Apps on the PlayStore that can be used to show the logcat
directly on your rooted phone. (Search for logcat). However in regards to further processing
(for example to create an issue here on Github) it is more convenient to just use your PC.
#### I found a bug
Please report it to our [issue tracker][issues]. If your app crashes please
provide a stack trace. If you are experiencing misbehaviour please provide
provide a stack trace. If you are experiencing misbehavior please provide
detailed steps to reproduce. Always mention whether you are running the latest
Play Store version or the current HEAD. If you are having problems connecting to
your XMPP server your file transfer doesnt work as expected please always

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

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="24"
height="24"
viewBox="0 0 24 24"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ic_notifications_none_white80.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1543"
inkscape:window-height="1093"
id="namedview6"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6c0-2.48 1.51-4.5 4-4.5s4 2.02 4 4.5v6z"
id="path4"
style="fill:#ffffff;fill-opacity:1;opacity:0.8" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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="24"
height="24"
viewBox="0 0 24 24"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ic_notifications_off_white80.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1244"
inkscape:window-height="936"
id="namedview6"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
d="M20 18.69L7.84 6.14 5.27 3.49 4 4.76l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.72l-1-1.03zM12 22c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2zm6-7.32V11c0-3.08-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.68z"
id="path4"
style="fill:#ffffff;fill-opacity:1;opacity:0.8" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

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="24"
height="24"
viewBox="0 0 24 24"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ic_notifications_paused_white80.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1375"
inkscape:window-height="999"
id="namedview6"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.93 6 11v5l-2 2v1h16v-1l-2-2zm-3.5-6.2l-2.8 3.4h2.8V15h-5v-1.8l2.8-3.4H9.5V8h5v1.8z"
id="path4"
style="fill:#ffffff;fill-opacity:1;opacity:0.8" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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="24"
height="24"
viewBox="0 0 24 24"
id="svg32"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ic_notifications_white80.svg">
<metadata
id="metadata40">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs38" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1471"
inkscape:window-height="985"
id="namedview36"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg32" />
<path
d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"
id="path34"
style="fill:#ffffff;fill-opacity:1;opacity:0.8" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

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_send_cancel_offline_white.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1080"
id="namedview6"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="-36.305085"
inkscape:cy="23.898305"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
d="M24 4C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm10 27.17L31.17 34 24 26.83 16.83 34 14 31.17 21.17 24 14 16.83 16.83 14 24 21.17 31.17 14 34 16.83 26.83 24 34 31.17z"
id="path4"
style="fill:#ffffff;fill-opacity:0.627451" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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_send_location_offline_white.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="956"
inkscape:window-height="1056"
id="namedview6"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="-36.305085"
inkscape:cy="23.898305"
inkscape:window-x="2880"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
d="M24 4c-7.73 0-14 6.27-14 14 0 10.5 14 26 14 26s14-15.5 14-26c0-7.73-6.27-14-14-14zm0 19c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"
id="path4"
style="fill:#ffffff;fill-opacity:0.627451" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
viewBox="0 0 48 48"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ic_send_photo_offline_white.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="956"
inkscape:window-height="567"
id="namedview8"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="10.5688"
inkscape:cy="23.898305"
inkscape:window-x="960"
inkscape:window-y="609"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<circle
cx="24"
cy="24"
r="6.4"
id="circle4"
style="fill:#ffffff;fill-opacity:0.627451" />
<path
d="M18 4l-3.66 4H8c-2.21 0-4 1.79-4 4v24c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V12c0-2.21-1.79-4-4-4h-6.34L30 4H18zm6 30c-5.52 0-10-4.48-10-10s4.48-10 10-10 10 4.48 10 10-4.48 10-10 10z"
id="path6"
style="fill:#ffffff;fill-opacity:0.627451" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
viewBox="0 0 48 48"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="ic_send_picture_offline_white.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1392"
id="namedview6"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="-21.864407"
inkscape:cy="23.898305"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg2" />
<path
d="M42 38V10c0-2.21-1.79-4-4-4H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4zM17 27l5 6.01L29 24l9 12H10l7-9z"
id="path4"
style="fill:#ffffff;fill-opacity:0.627451" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg3621"
version="1.1"
inkscape:version="0.91 r13725"
width="96"
height="96"
sodipodi:docname="ic_send_text_offline_white.svg"
inkscape:export-filename="/home/daniel/workspace/Conversations/res/drawable-xxhdpi/ic_action_send_now_online.png"
inkscape:export-xdpi="154.28572"
inkscape:export-ydpi="154.28572">
<metadata
id="metadata3627">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3625" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1344"
inkscape:window-height="1056"
id="namedview3623"
showgrid="true"
showguides="true"
inkscape:zoom="8"
inkscape:cx="31.783303"
inkscape:cy="56.698828"
inkscape:window-x="2880"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg3621"
inkscape:snap-others="false">
<inkscape:grid
type="xygrid"
id="grid3631" />
</sodipodi:namedview>
<path
style="fill:#ffffff;fill-opacity:0.627451;stroke:none"
d="M 3.887575,4.1549246 90.999747,47.676331 3.887575,91.286663 13.203552,52.344101 63.012683,47.720794 13.203552,43.008558 Z"
id="path3633"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccc"
inkscape:export-filename="/home/daniel/workspace/Conversations/res/drawable-mdpi/ic_action_send_now_dnd.png"
inkscape:export-xdpi="51.42857"
inkscape:export-ydpi="51.42857" />
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

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_send_voice_offline_white.svg">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1516"
inkscape:window-height="1056"
id="namedview6"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="-36.711864"
inkscape:cy="24"
inkscape:window-x="2880"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
d="M24 30c3.31 0 5.98-2.69 5.98-6L30 12c0-3.32-2.68-6-6-6-3.31 0-6 2.68-6 6v12c0 3.31 2.69 6 6 6zm10.6-6c0 6-5.07 10.2-10.6 10.2-5.52 0-10.6-4.2-10.6-10.2H10c0 6.83 5.44 12.47 12 13.44V44h4v-6.56c6.56-.97 12-6.61 12-13.44h-3.4z"
id="path4"
style="fill:#ffffff;fill-opacity:0.627451" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

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

1
art/main_logo.svg Symbolic link
View file

@ -0,0 +1 @@
ic_launcher.svg

View file

@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="36"
height="26"
id="svg2"
version="1.1"
inkscape:version="0.48.5 r10040"
sodipodi:docname="message_bubble_received.svg">
<defs
id="defs4">
<filter
x="-0.25"
y="-0.25"
width="1.5"
height="1.5"
inkscape:label="Drop Shadow"
id="filter3811"
color-interpolation-filters="sRGB">
<feFlood
flood-opacity="0.25"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood3813" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite3815" />
<feGaussianBlur
stdDeviation="0.5"
result="blur"
id="feGaussianBlur3817" />
<feOffset
dx="0"
dy="1"
result="offset"
id="feOffset3819" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite3821" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="25.745257"
inkscape:cy="9.618802"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="989"
inkscape:window-height="755"
inkscape:window-x="22"
inkscape:window-y="16"
inkscape:window-maximized="0"
showguides="true"
inkscape:guide-bbox="true"
guidecolor="#000000"
guideopacity="0.49803922">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="4"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="1px"
spacingy="1px"
originx="0px"
originy="0px"
color="#0000ff"
opacity="0.03137255" />
<sodipodi:guide
orientation="1,0"
position="20,26"
id="guide3060" />
<sodipodi:guide
orientation="1,0"
position="24,26"
id="guide3062" />
<sodipodi:guide
orientation="0,1"
position="36,22"
id="guide3064" />
<sodipodi:guide
orientation="0,1"
position="36,6"
id="guide3066" />
<sodipodi:guide
orientation="1,0"
position="26,0"
id="guide3068" />
<sodipodi:guide
orientation="1,0"
position="18,0"
id="guide3070" />
<sodipodi:guide
orientation="0,1"
position="0,10"
id="guide3074" />
<sodipodi:guide
orientation="0,1"
position="0,8"
id="guide3076" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer"
inkscape:groupmode="layer"
id="layer"
transform="translate(0,-2)">
<g
id="g3759"
style="fill:#326130;fill-opacity:1;stroke:none;fill-rule:nonzero;filter:url(#filter3811)">
<path
style="display:none"
d="m 8,6 c 2,2 4,6 4,10 L 16,6 z"
id="path3805"
inkscape:connector-curvature="0"
transform="translate(0,2)"
sodipodi:nodetypes="cccc" />
<path
inkscape:connector-curvature="0"
id="path2989"
d="M 4,4 16,16 16,4 z"
sodipodi:nodetypes="cccc" />
<rect
ry="2"
y="4"
x="12"
height="20"
width="20"
id="rect2987" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="36"
height="26"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="message_bubble_received_grey.svg">
<defs
id="defs4">
<filter
x="-0.25"
y="-0.25"
width="1.5"
height="1.5"
inkscape:label="Drop Shadow"
id="filter3811"
color-interpolation-filters="sRGB">
<feFlood
flood-opacity="0.25"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood3813" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite3815" />
<feGaussianBlur
stdDeviation="0.5"
result="blur"
id="feGaussianBlur3817" />
<feOffset
dx="0"
dy="1"
result="offset"
id="feOffset3819" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite3821" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="-9.879743"
inkscape:cy="9.618802"
inkscape:document-units="px"
inkscape:current-layer="layer"
showgrid="true"
inkscape:window-width="2135"
inkscape:window-height="911"
inkscape:window-x="22"
inkscape:window-y="16"
inkscape:window-maximized="0"
showguides="true"
inkscape:guide-bbox="true"
guidecolor="#000000"
guideopacity="0.49803922">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="4"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="1px"
spacingy="1px"
originx="0px"
originy="0px"
color="#0000ff"
opacity="0.03137255" />
<sodipodi:guide
orientation="1,0"
position="20,26"
id="guide3060" />
<sodipodi:guide
orientation="1,0"
position="24,26"
id="guide3062" />
<sodipodi:guide
orientation="0,1"
position="36,22"
id="guide3064" />
<sodipodi:guide
orientation="0,1"
position="36,6"
id="guide3066" />
<sodipodi:guide
orientation="1,0"
position="26,0"
id="guide3068" />
<sodipodi:guide
orientation="1,0"
position="18,0"
id="guide3070" />
<sodipodi:guide
orientation="0,1"
position="0,10"
id="guide3074" />
<sodipodi:guide
orientation="0,1"
position="0,8"
id="guide3076" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer"
inkscape:groupmode="layer"
id="layer"
transform="translate(0,-2)">
<g
id="g3759"
style="fill:#424242;fill-opacity:1;stroke:none;fill-rule:nonzero;filter:url(#filter3811)">
<path
style="display:none;fill:#424242;fill-opacity:1"
d="m 8,6 c 2,2 4,6 4,10 L 16,6 z"
id="path3805"
inkscape:connector-curvature="0"
transform="translate(0,2)"
sodipodi:nodetypes="cccc" />
<path
inkscape:connector-curvature="0"
id="path2989"
d="M 4,4 16,16 16,4 z"
sodipodi:nodetypes="cccc"
style="fill:#424242;fill-opacity:1" />
<rect
ry="2"
y="4"
x="12"
height="20"
width="20"
id="rect2987"
style="fill:#424242;fill-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -0,0 +1,167 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="36"
height="26"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="message_bubble_sent_grey.svg">
<defs
id="defs4">
<filter
x="-0.25"
y="-0.25"
width="1.5"
height="1.5"
inkscape:label="Drop Shadow"
id="filter3811"
color-interpolation-filters="sRGB">
<feFlood
flood-opacity="0.25"
flood-color="rgb(0,0,0)"
result="flood"
id="feFlood3813" />
<feComposite
in="flood"
in2="SourceGraphic"
operator="in"
result="composite1"
id="feComposite3815" />
<feGaussianBlur
stdDeviation="0.5"
result="blur"
id="feGaussianBlur3817" />
<feOffset
dx="0"
dy="1"
result="offset"
id="feOffset3819" />
<feComposite
in="SourceGraphic"
in2="offset"
operator="over"
result="composite2"
id="feComposite3821" />
</filter>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="16"
inkscape:cx="6.244862"
inkscape:cy="16.118802"
inkscape:document-units="px"
inkscape:current-layer="layer"
showgrid="true"
inkscape:window-width="1554"
inkscape:window-height="900"
inkscape:window-x="878"
inkscape:window-y="369"
inkscape:window-maximized="0"
showguides="true"
inkscape:guide-bbox="true"
guidecolor="#404040"
guideopacity="0.49803922">
<inkscape:grid
type="xygrid"
id="grid2985"
empspacing="4"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="1px"
spacingy="1px"
originx="0px"
originy="0px"
color="#0000ff"
opacity="0.03137255" />
<sodipodi:guide
orientation="1,0"
position="12,26"
id="guide3146" />
<sodipodi:guide
orientation="1,0"
position="16,26"
id="guide3148" />
<sodipodi:guide
orientation="0,1"
position="36,22"
id="guide3150" />
<sodipodi:guide
orientation="0,1"
position="36,6"
id="guide3152" />
<sodipodi:guide
orientation="1,0"
position="18,0"
id="guide3154" />
<sodipodi:guide
orientation="1,0"
position="10,0"
id="guide3160" />
<sodipodi:guide
orientation="0,1"
position="0,20"
id="guide3162" />
<sodipodi:guide
orientation="0,1"
position="0,18"
id="guide3164" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer"
inkscape:groupmode="layer"
id="layer"
transform="translate(0,-2)">
<g
id="g3759"
style="fill:#424242;fill-opacity:1;stroke:none;fill-rule:nonzero;filter:url(#filter3811)">
<path
style="display:none;fill:#424242;fill-opacity:1"
d="M 28,18 C 26,16 24,12 24,8 l -4,10 z"
id="path3809"
inkscape:connector-curvature="0"
transform="translate(0,2)"
sodipodi:nodetypes="cccc" />
<path
inkscape:connector-curvature="0"
id="path2989"
d="m 20,12 0,12 12,0 z"
sodipodi:nodetypes="cccc"
style="fill:#424242;fill-opacity:1" />
<rect
ry="2"
y="4"
x="4"
height="20"
width="20"
id="rect2987"
style="fill:#424242;fill-opacity:1" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

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

59
art/play_video.svg Normal file
View file

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
viewBox="0 0 48 48"
id="svg2"
version="1.1"
inkscape:version="0.91 r13725"
sodipodi:docname="play_video.svg">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1916"
inkscape:window-height="1156"
id="namedview8"
showgrid="false"
inkscape:zoom="4.9166667"
inkscape:cx="0.91525424"
inkscape:cy="24"
inkscape:window-x="0"
inkscape:window-y="20"
inkscape:window-maximized="0"
inkscape:current-layer="svg2" />
<path
d="M0 0h48v48H0z"
fill="none"
id="path4" />
<path
d="M20 33l12-9-12-9v18zm4-29C12.95 4 4 12.95 4 24s8.95 20 20 20 20-8.95 20-20S35.05 4 24 4zm0 36c-8.82 0-16-7.18-16-16S15.18 8 24 8s16 7.18 16 16-7.18 16-16 16z"
id="path6"
style="fill:#ffffff;fill-opacity:0.7019608;opacity:1;stroke:none;stroke-opacity:0.38039216" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -11,42 +11,59 @@ resolutions = {
}
images = {
'conversations_baloon.svg' => ['ic_launcher', 48],
'ic_launcher.svg' => ['ic_launcher', 48],
'main_logo.svg' => ['main_logo', 200],
'play_video.svg' => ['play_video', 128],
'play_gif.svg' => ['play_gif', 128],
'conversations_mono.svg' => ['ic_notification', 24],
'ic_received_indicator.svg' => ['ic_received_indicator', 12],
'ic_send_text_offline.svg' => ['ic_send_text_offline', 36],
'ic_send_text_offline_white.svg' => ['ic_send_text_offline_white', 36],
'ic_send_text_online.svg' => ['ic_send_text_online', 36],
'ic_send_text_away.svg' => ['ic_send_text_away', 36],
'ic_send_text_dnd.svg' => ['ic_send_text_dnd', 36],
'ic_send_photo_online.svg' => ['ic_send_photo_online', 36],
'ic_send_photo_offline.svg' => ['ic_send_photo_offline', 36],
'ic_send_photo_offline_white.svg' => ['ic_send_photo_offline_white', 36],
'ic_send_photo_away.svg' => ['ic_send_photo_away', 36],
'ic_send_photo_dnd.svg' => ['ic_send_photo_dnd', 36],
'ic_send_location_online.svg' => ['ic_send_location_online', 36],
'ic_send_location_offline.svg' => ['ic_send_location_offline', 36],
'ic_send_location_offline_white.svg' => ['ic_send_location_offline_white', 36],
'ic_send_location_away.svg' => ['ic_send_location_away', 36],
'ic_send_location_dnd.svg' => ['ic_send_location_dnd', 36],
'ic_send_voice_online.svg' => ['ic_send_voice_online', 36],
'ic_send_voice_offline.svg' => ['ic_send_voice_offline', 36],
'ic_send_voice_offline_white.svg' => ['ic_send_voice_offline_white', 36],
'ic_send_voice_away.svg' => ['ic_send_voice_away', 36],
'ic_send_voice_dnd.svg' => ['ic_send_voice_dnd', 36],
'ic_send_cancel_online.svg' => ['ic_send_cancel_online', 36],
'ic_send_cancel_offline.svg' => ['ic_send_cancel_offline', 36],
'ic_send_cancel_offline_white.svg' => ['ic_send_cancel_offline_white', 36],
'ic_send_cancel_away.svg' => ['ic_send_cancel_away', 36],
'ic_send_cancel_dnd.svg' => ['ic_send_cancel_dnd', 36],
'ic_send_picture_online.svg' => ['ic_send_picture_online', 36],
'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36],
'ic_send_picture_offline_white.svg' => ['ic_send_picture_offline_white', 36],
'ic_send_picture_away.svg' => ['ic_send_picture_away', 36],
'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36],
'ic_notifications_none_white80.svg' => ['ic_notifications_none_white80', 24],
'ic_notifications_off_white80.svg' => ['ic_notifications_off_white80', 24],
'ic_notifications_paused_white80.svg' => ['ic_notifications_paused_white80', 24],
'ic_notifications_white80.svg' => ['ic_notifications_white80', 24],
'ic_verified_fingerprint.svg' => ['ic_verified_fingerprint', 36],
'md_switch_thumb_disable.svg' => ['switch_thumb_disable', 48],
'md_switch_thumb_off_normal.svg' => ['switch_thumb_off_normal', 48],
'md_switch_thumb_off_pressed.svg' => ['switch_thumb_off_pressed', 48],
'md_switch_thumb_on_normal.svg' => ['switch_thumb_on_normal', 48],
'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48],
'message_bubble_received.svg' => ['message_bubble_received.9', 0],
'message_bubble_received_grey.svg' => ['message_bubble_received_grey.9', 0],
'message_bubble_received_dark.svg' => ['message_bubble_received_dark.9', 0],
'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0],
'message_bubble_received_white.svg' => ['message_bubble_received_white.9', 0],
'message_bubble_sent.svg' => ['message_bubble_sent.9', 0],
'message_bubble_sent_grey.svg' => ['message_bubble_sent_grey.9', 0],
}
# Executable paths for Mac OSX

View file

@ -1,119 +1,118 @@
// Top-level build file where you can add configuration options common to all
// sub-projects/modules.
buildscript {
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.3.1'
}
}
allprojects {
repositories {
jcenter()
mavenCentral()
maven {
url 'http://lorenzo.villani.me/android-cropimage/'
}
}
repositories {
jcenter()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0'
}
}
apply plugin: 'com.android.application'
repositories {
jcenter()
mavenCentral()
jcenter()
mavenCentral()
}
configurations {
playstoreCompile
}
dependencies {
compile project(':libs:MemorizingTrustManager')
compile 'org.sufficientlysecure:openpgp-api:10.0'
compile 'com.soundcloud.android:android-crop:1.0.1@aar'
compile 'com.android.support:support-v13:23.0.1'
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
compile 'org.bouncycastle:bcmail-jdk15on:1.52'
compile 'org.jitsi:org.otr4j:0.22'
compile 'org.gnu.inet:libidn:1.15'
compile 'com.google.zxing:core:3.2.1'
compile 'com.google.zxing:android-integration:3.2.1'
compile 'de.measite.minidns:minidns:0.1.7'
compile 'de.timroes.android:EnhancedListView:0.3.4'
compile 'me.leolin:ShortcutBadger:1.1.3@aar'
compile 'com.kyleduo.switchbutton:library:1.2.8'
compile 'org.whispersystems:axolotl-android:1.3.4'
compile 'com.makeramen:roundedimageview:2.2.0'
compile project(':libs:MemorizingTrustManager')
playstoreCompile 'com.google.android.gms:play-services-gcm:10.0.1'
compile 'org.sufficientlysecure:openpgp-api:10.0'
compile 'com.soundcloud.android:android-crop:1.0.1@aar'
compile 'com.android.support:support-v13:25.1.0'
compile 'org.bouncycastle:bcprov-jdk15on:1.52'
compile 'org.bouncycastle:bcmail-jdk15on:1.52'
compile 'org.jitsi:org.otr4j:0.22'
compile 'org.gnu.inet:libidn:1.15'
compile 'com.google.zxing:core:3.2.1'
compile 'com.google.zxing:android-integration:3.2.1'
compile 'de.measite.minidns:minidns:0.1.7'
compile 'de.timroes.android:EnhancedListView:0.3.4'
compile 'me.leolin:ShortcutBadger:1.1.11@aar'
compile 'com.kyleduo.switchbutton:library:1.2.8'
compile 'org.whispersystems:axolotl-android:1.3.4'
compile 'com.makeramen:roundedimageview:2.2.0'
compile "com.wefika:flowlayout:0.4.1"
compile 'net.ypresto.androidtranscoder:android-transcoder:0.2.0'
}
ext {
travisBuild = System.getenv("TRAVIS") == "true"
// allows for -Dpre-dex=false to be set
preDexEnabled = "true".equals(System.getProperty("pre-dex", "true"))
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.2"
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 14
targetSdkVersion 23
versionCode 122
versionName "1.9.3"
project.ext.set(archivesBaseName, archivesBaseName + "-" + versionName);
}
defaultConfig {
minSdkVersion 14
targetSdkVersion 25
versionCode 193
versionName "1.15.5"
archivesBaseName += "-$versionName"
applicationId "eu.siacs.conversations"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
dexOptions {
// Skip pre-dexing when running on Travis CI or when disabled via -Dpre-dex=false.
preDexLibraries = preDexEnabled && !travisBuild
}
//
// To sign release builds, create the file `gradle.properties` in
// $HOME/.gradle or in your project directory with this content:
//
// mStoreFile=/path/to/key.store
// mStorePassword=xxx
// mKeyAlias=alias
// mKeyPassword=xxx
//
if (project.hasProperty('mStoreFile') &&
project.hasProperty('mStorePassword') &&
project.hasProperty('mKeyAlias') &&
project.hasProperty('mKeyPassword')) {
signingConfigs {
release {
storeFile file(mStoreFile)
storePassword mStorePassword
keyAlias mKeyAlias
keyPassword mKeyPassword
}
}
buildTypes.release.signingConfig = signingConfigs.release
} else {
buildTypes.release.signingConfig = null
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
applicationVariants.all { variant ->
if (variant.name.equals('release')) {
variant.outputs.each { output ->
if (output.zipAlign != null) {
output.zipAlign.outputFile = new File(output.outputFile.parent, rootProject.name + "-${variant.versionName}.apk")
}
}
}
}
productFlavors {
playstore
free
}
if (project.hasProperty('mStoreFile') &&
project.hasProperty('mStorePassword') &&
project.hasProperty('mKeyAlias') &&
project.hasProperty('mKeyPassword')) {
signingConfigs {
release {
storeFile file(mStoreFile)
storePassword mStorePassword
keyAlias mKeyAlias
keyPassword mKeyPassword
}
}
buildTypes.release.signingConfig = signingConfigs.release
} else {
buildTypes.release.signingConfig = null
}
lintOptions {
disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource'
}
lintOptions {
disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource'
}
subprojects {
subprojects {
afterEvaluate {
if (getPlugins().hasPlugin('android') ||
getPlugins().hasPlugin('android-library')) {
afterEvaluate {
if (getPlugins().hasPlugin('android') ||
getPlugins().hasPlugin('android-library')) {
configure(android.lintOptions) {
disable 'AndroidGradlePluginVersion', 'MissingTranslation'
}
}
configure(android.lintOptions) {
disable 'AndroidGradlePluginVersion', 'MissingTranslation'
}
}
}
}
}
}
packagingOptions {
exclude 'META-INF/BCKEY.DSA'
exclude 'META-INF/BCKEY.SF'
}
}

View file

@ -2,13 +2,17 @@
* XEP-0030: Service Discovery
* XEP-0045: Multi-User Chat
* XEP-0048: Bookmarks
* XEP-0084: User Avatar
* XEP-0085: Chat State Notifications
* XEP-0092: Software Version
* XEP-0115: Entity Capabilities
* XEP-0163: Personal Eventing Protocol (avatars and nicks)
* XEP-0166: Jingle (only used for file transfer)
* XEP-0172: User Nickname
* XEP-0184: Message Delivery Receipts (reply only)
* XEP-0191: Blocking command
* XEP-0198: Stream Management
* XEP-0199: XMPP Ping
* XEP-0234: Jingle File Transfer
* XEP-0237: Roster Versioning
* XEP-0245: The /me Command
@ -16,7 +20,13 @@
* XEP-0260: Jingle SOCKS5 Bytestreams Transport Method
* XEP-0261: Jingle In-Band Bytestreams Transport Method
* XEP-0280: Message Carbons
* XEP-0308: Last Message Correction
* XEP-0313: Message Archive Management
* XEP-0319: Last User Interaction in Presence
* XEP-0333: Chat Markers
* XEP-0352: Client State Indication
* XEP-0191: Blocking command
* XEP-0357: Push Notifications
* XEP-0363: HTTP File Upload
* XEP-0368: SRV records for XMPP over TLS
* XEP-0377: Spam Reporting
* XEP-0384: OMEMO Encryption

View file

@ -1,6 +1,6 @@
#Sat Nov 22 17:47:57 CET 2014
#Sat Apr 09 13:15:52 CEST 2016
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-2.12-all.zip

View file

@ -3,18 +3,18 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
classpath 'com.android.tools.build:gradle:2.0.0'
}
}
apply plugin: 'android-library'
apply plugin: 'com.android.library'
android {
compileSdkVersion 19
buildToolsVersion "19.1"
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 7
targetSdkVersion 19
minSdkVersion 14
targetSdkVersion 25
}
sourceSets {

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);
}
}
});
@ -702,28 +833,45 @@ public class MemorizingTrustManager implements X509TrustManager {
}
@Override
public boolean verify(String hostname, SSLSession session) {
return verify(hostname, session, true);
return verify(hostname, session, false);
}
}
public X509TrustManager getNonInteractive(String domain) {
return new NonInteractiveMemorizingTrustManager(domain);
}
public X509TrustManager getInteractive(String domain) {
return new InteractiveMemorizingTrustManager(domain);
}
public X509TrustManager getNonInteractive() {
return new NonInteractiveMemorizingTrustManager();
return new NonInteractiveMemorizingTrustManager(null);
}
public X509TrustManager getInteractive() {
return new InteractiveMemorizingTrustManager(null);
}
private class NonInteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;
public NonInteractiveMemorizingTrustManager(String domain) {
this.domain = domain;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false);
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false);
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false);
}
@Override
@ -732,4 +880,28 @@ public class MemorizingTrustManager implements X509TrustManager {
}
}
private class InteractiveMemorizingTrustManager implements X509TrustManager {
private final String domain;
public InteractiveMemorizingTrustManager(String domain) {
this.domain = domain;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return MemorizingTrustManager.this.getAcceptedIssuers();
}
}
}

View file

@ -1,20 +0,0 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

View file

@ -1,27 +0,0 @@
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in /home/sam/android-sdk-linux/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
-dontwarn javax.naming.**
-keep class * extends java.util.ListResourceBundle {
protected Object[][] getContents();
}
-keepnames class * implements android.os.Parcelable {
public static final ** CREATOR;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,011 KiB

After

Width:  |  Height:  |  Size: 195 KiB

View file

@ -0,0 +1,28 @@
package eu.siacs.conversations.services;
import eu.siacs.conversations.entities.Account;
public class PushManagementService {
protected final XmppConnectionService mXmppConnectionService;
public PushManagementService(XmppConnectionService service) {
this.mXmppConnectionService = service;
}
public void registerPushTokenOnServer(Account account) {
//stub implementation. only affects playstore flavor
}
public boolean available(Account account) {
return false;
}
public boolean isStub() {
return true;
}
public boolean availableAndUseful(Account account) {
return false;
}
}

View file

@ -1,24 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
package="eu.siacs.conversations"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.siacs.conversations">
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.READ_PROFILE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission
android:name="android.permission.READ_PHONE_STATE"
tools:node="remove"/>
tools:node="remove" />
<uses-sdk tools:overrideLibrary="net.ypresto.androidtranscoder" />
<application
android:allowBackup="true"
@ -26,14 +27,14 @@
android:label="@string/app_name"
android:theme="@style/ConversationsTheme"
tools:replace="android:label">
<service android:name=".services.XmppConnectionService"/>
<service android:name=".services.XmppConnectionService" />
<receiver android:name=".services.EventReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
<action android:name="android.intent.action.ACTION_SHUTDOWN"/>
<action android:name="android.media.RINGER_MODE_CHANGED"/>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.intent.action.ACTION_SHUTDOWN" />
<action android:name="android.media.RINGER_MODE_CHANGED" />
</intent-filter>
</receiver>
@ -41,102 +42,130 @@
android:name=".ui.ConversationActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:minWidth="300dp"
android:minHeight="300dp"
android:windowSoftInputMode="stateHidden">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER"/>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.StartConversationActivity"
android:configChanges="orientation|screenSize"
android:label="@string/title_activity_start_conversation"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.SENDTO"/>
<action android:name="android.intent.action.SENDTO" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="imto"/>
<data android:host="jabber"/>
<data android:scheme="imto" />
<data android:host="jabber" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="xmpp"/>
<data android:scheme="xmpp" />
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="xmpp"/>
<data android:scheme="xmpp" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
<data android:host="conversations.im" />
<data android:pathPrefix="/i/" />
<data android:pathPrefix="/j/" />
</intent-filter>
</activity>
<activity
android:name=".ui.WelcomeActivity"
android:label="@string/app_name"
android:launchMode="singleTask"/>
<activity
android:name=".ui.MagicCreateActivity"
android:label="@string/create_account"
android:launchMode="singleTask"/>
<activity
android:name=".ui.SetPresenceActivity"
android:configChanges="orientation|screenSize"
android:label="@string/change_presence"
android:launchMode="singleTask"
android:windowSoftInputMode="stateHidden|adjustResize" />
<activity
android:name=".ui.SettingsActivity"
android:label="@string/title_activity_settings"/>
android:label="@string/title_activity_settings" />
<activity
android:name=".ui.ChooseContactActivity"
android:label="@string/title_activity_choose_contact"/>
android:label="@string/title_activity_choose_contact" />
<activity
android:name=".ui.BlocklistActivity"
android:label="@string/title_activity_block_list"/>
android:label="@string/title_activity_block_list" />
<activity
android:name=".ui.ChangePasswordActivity"
android:label="@string/change_password_on_server"/>
android:label="@string/change_password_on_server" />
<activity
android:name=".ui.ManageAccountActivity"
android:label="@string/title_activity_manage_accounts"
android:launchMode="singleTask"/>
android:launchMode="singleTask" />
<activity
android:name=".ui.EditAccountActivity"
android:launchMode="singleTask"
android:windowSoftInputMode="stateHidden|adjustResize"/>
android:windowSoftInputMode="stateHidden|adjustResize" />
<activity
android:name=".ui.ConferenceDetailsActivity"
android:label="@string/title_activity_conference_details"
android:windowSoftInputMode="stateHidden"/>
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.ContactDetailsActivity"
android:label="@string/title_activity_contact_details"
android:windowSoftInputMode="stateHidden"/>
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.PublishProfilePictureActivity"
android:label="@string/mgmt_account_publish_avatar"
android:windowSoftInputMode="stateHidden"/>
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.VerifyOTRActivity"
android:label="@string/verify_otr"
android:windowSoftInputMode="stateHidden"/>
android:windowSoftInputMode="stateHidden" />
<activity
android:name=".ui.ShareWithActivity"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain"/>
<data android:mimeType="text/plain" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*"/>
<data android:mimeType="*/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*"/>
<data android:mimeType="image/*" />
</intent-filter>
<meta-data
android:name="android.service.chooser.chooser_target_service"
android:value=".services.ContactChooserTargetService" />
@ -144,27 +173,46 @@
<activity
android:name=".ui.TrustKeysActivity"
android:label="@string/trust_omemo_fingerprints"
android:windowSoftInputMode="stateAlwaysHidden"/>
android:windowSoftInputMode="stateAlwaysHidden" />
<activity
android:name="de.duenndns.ssl.MemorizingActivity"
android:theme="@style/ConversationsTheme"
tools:replace="android:theme"/>
tools:replace="android:theme" />
<activity
android:name=".ui.AboutActivity"
android:label="@string/title_activity_about"
android:parentActivityName=".ui.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="eu.siacs.conversations.ui.SettingsActivity"/>
android:value="eu.siacs.conversations.ui.SettingsActivity" />
</activity>
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
<service android:name=".services.ExportLogsService"/>
<service android:name=".services.ContactChooserTargetService"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
<service android:name=".services.ExportLogsService" />
<service
android:name=".services.ContactChooserTargetService"
android:permission="android.permission.BIND_CHOOSER_TARGET_SERVICE">
<intent-filter>
<action android:name="android.service.chooser.ChooserTargetService" />
</intent-filter>
</service>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.files"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<provider
android:authorities="${applicationId}.barcodes"
android:name=".services.BarcodeProvider"
android:exported="false"
android:grantUriPermissions="true"/>
</application>
</manifest>

View file

@ -6,29 +6,59 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
public final class Config {
private static final int UNENCRYPTED = 1;
private static final int OPENPGP = 2;
private static final int OTR = 4;
private static final int OMEMO = 8;
private static final int ENCRYPTION_MASK = UNENCRYPTED | OPENPGP | OTR | OMEMO;
public static boolean supportUnencrypted() {
return (ENCRYPTION_MASK & UNENCRYPTED) != 0;
}
public static boolean supportOpenPgp() {
return (ENCRYPTION_MASK & OPENPGP) != 0;
}
public static boolean supportOtr() {
return (ENCRYPTION_MASK & OTR) != 0;
}
public static boolean supportOmemo() {
return (ENCRYPTION_MASK & OMEMO) != 0;
}
public static boolean multipleEncryptionChoices() {
return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
}
public static final String LOGTAG = "conversations";
public static final String BUG_REPORTS = "bugs@conversations.im";
public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
public static final String MAGIC_CREATE_DOMAIN = "conversations.im";
public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
public static final boolean HIDE_PGP_IN_UI = false; //some more consumer focused clients might want to disable OpenPGP
public static final boolean FORCE_E2E_ENCRYPTION = false; //disables ability to send unencrypted 1-on-1
public static final boolean ALLOW_NON_TLS_CONNECTIONS = false; //very dangerous. you should have a good reason to set this to true
public static final boolean FORCE_ORBOT = false; // always use TOR
public static final boolean HIDE_MESSAGE_TEXT_IN_NOTIFICATION = false;
public static final boolean SHOW_CONNECTED_ACCOUNTS = false; //show number of connected accounts in foreground notification
public static final boolean SHOW_DISABLE_FOREGROUND = false; //if set to true the foreground notification has a button to disable it
public static final boolean ALWAYS_NOTIFY_BY_DEFAULT = false;
public static final boolean LEGACY_NAMESPACE_HTTP_UPLOAD = false;
public static final int PING_MAX_INTERVAL = 300;
public static final int IDLE_PING_INTERVAL = 600; //540 is minimum according to docs;
public static final int PING_MIN_INTERVAL = 30;
public static final int LOW_PING_TIMEOUT = 1; // used after push received
public static final int PING_TIMEOUT = 15;
public static final int SOCKET_TIMEOUT = 15;
public static final int CONNECT_TIMEOUT = 90;
public static final int CONNECT_DISCO_TIMEOUT = 20;
public static final int CARBON_GRACE_PERIOD = 90;
public static final int MINI_GRACE_PERIOD = 750;
public static final int AVATAR_SIZE = 192;
@ -46,29 +76,46 @@ public final class Config {
public static final int REFRESH_UI_INTERVAL = 500;
public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096;
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
public static final long OMEMO_AUTO_EXPIRY = 7 * MILLISECONDS_IN_DAY;
public static final boolean REMOVE_BROKEN_DEVICES = false;
public static final boolean OMEMO_PADDING = false;
public static boolean PUT_AUTH_TAG_INTO_KEY = false;
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
public static final boolean BACKGROUND_STANZA_LOGGING = false; //log all stanzas that were received while the app is in background
public static final boolean RESET_ATTEMPT_COUNT_ON_NETWORK_CHANGE = true; //setting to true might increase power consumption
public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
public static final boolean REPORT_WRONG_FILESIZE_IN_OTR_JINGLE = true;
public static final boolean SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON = false;
public static final boolean X509_VERIFICATION = false; //use x509 certificates to verify OMEMO keys
public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
public static final boolean PARSE_REAL_JID_FROM_MUC_MAM = false; //dangerous if server doesnt filter
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
public static final int MAM_MAX_MESSAGES = 500;
public static final long FREQUENT_RESTARTS_DETECTION_WINDOW = 12 * 60 * 60 * 1000; // 10 hours
public static final long FREQUENT_RESTARTS_THRESHOLD = 0; // previous value was 16;
public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
public static final int TYPING_TIMEOUT = 8;
public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
public static final String ENABLED_CIPHERS[] = {
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384",
@ -107,6 +154,5 @@ public final class Config {
};
private Config() {
}
}

View file

@ -53,28 +53,30 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
this.mXmppConnectionService = service;
}
private KeyPair loadKey(JSONObject keys) {
private KeyPair loadKey(final JSONObject keys) {
if (keys == null) {
return null;
}
try {
BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
return new KeyPair(publicKey, privateKey);
} catch (JSONException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (InvalidKeySpecException e) {
return null;
synchronized (keys) {
try {
BigInteger x = new BigInteger(keys.getString("otr_x"), 16);
BigInteger y = new BigInteger(keys.getString("otr_y"), 16);
BigInteger p = new BigInteger(keys.getString("otr_p"), 16);
BigInteger q = new BigInteger(keys.getString("otr_q"), 16);
BigInteger g = new BigInteger(keys.getString("otr_g"), 16);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
return new KeyPair(publicKey, privateKey);
} catch (JSONException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (InvalidKeySpecException e) {
return null;
}
}
}
@ -122,7 +124,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
@Override
public String getFallbackMessage(SessionID arg0) {
return "I would like to start a private (OTR encrypted) conversation but your client doesnt seem to support that";
return MessageGenerator.OTR_FALLBACK_MESSAGE;
}
@Override
@ -192,8 +194,9 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
} catch (final InvalidJidException ignored) {
}
packet.setType(MessagePacket.TYPE_CHAT);
packet.addChild("encryption","urn:xmpp:eme:0")
.setAttribute("namespace","urn:xmpp:otr:0");
account.getXmppConnection().sendMessagePacket(packet);
}

View file

@ -1,162 +1,219 @@
package eu.siacs.conversations.crypto;
import android.app.PendingIntent;
import android.content.Intent;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
import org.openintents.openpgp.util.OpenPgpApi;
import java.util.Collections;
import java.util.LinkedList;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
public class PgpDecryptionService {
private final XmppConnectionService xmppConnectionService;
private final ConcurrentHashMap<String, List<Message>> messages = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Boolean> decryptingMessages = new ConcurrentHashMap<>();
private Boolean keychainLocked = false;
private final Object keychainLockedLock = new Object();
private final XmppConnectionService mXmppConnectionService;
private OpenPgpApi openPgpApi = null;
public PgpDecryptionService(XmppConnectionService xmppConnectionService) {
this.xmppConnectionService = xmppConnectionService;
protected final ArrayDeque<Message> messages = new ArrayDeque();
protected final HashSet<Message> pendingNotifications = new HashSet<>();
Message currentMessage;
private PendingIntent pendingIntent;
public PgpDecryptionService(XmppConnectionService service) {
this.mXmppConnectionService = service;
}
public synchronized boolean decrypt(final Message message, boolean notify) {
messages.add(message);
if (notify && pendingIntent == null) {
pendingNotifications.add(message);
continueDecryption();
return false;
} else {
continueDecryption();
return notify;
}
}
public void add(Message message) {
if (isRunning()) {
decryptDirectly(message);
} else {
store(message);
public synchronized void decrypt(final List<Message> list) {
for(Message message : list) {
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
messages.add(message);
}
}
continueDecryption();
}
public synchronized void discard(List<Message> discards) {
this.messages.removeAll(discards);
this.pendingNotifications.removeAll(discards);
}
public synchronized void discard(Message message) {
this.messages.remove(message);
this.pendingNotifications.remove(message);
}
protected synchronized void decryptNext() {
if (pendingIntent == null
&& getOpenPgpApi() != null
&& (currentMessage = messages.poll()) != null) {
new Thread(new Runnable() {
@Override
public void run() {
executeApi(currentMessage);
decryptNext();
}
}).start();
}
}
public void addAll(List<Message> messagesList) {
if (!messagesList.isEmpty()) {
String conversationUuid = messagesList.get(0).getConversation().getUuid();
if (!messages.containsKey(conversationUuid)) {
List<Message> list = Collections.synchronizedList(new LinkedList<Message>());
messages.put(conversationUuid, list);
}
synchronized (messages.get(conversationUuid)) {
messages.get(conversationUuid).addAll(messagesList);
}
decryptAllMessages();
}
}
public synchronized void continueDecryption(boolean resetPending) {
if (resetPending) {
this.pendingIntent = null;
}
continueDecryption();
}
public void onKeychainUnlocked() {
synchronized (keychainLockedLock) {
keychainLocked = false;
}
decryptAllMessages();
}
public synchronized void continueDecryption() {
if (currentMessage == null) {
decryptNext();
}
}
public void onKeychainLocked() {
synchronized (keychainLockedLock) {
keychainLocked = true;
}
xmppConnectionService.updateConversationUi();
}
private synchronized OpenPgpApi getOpenPgpApi() {
if (openPgpApi == null) {
this.openPgpApi = mXmppConnectionService.getOpenPgpApi();
}
return this.openPgpApi;
}
public void onOpenPgpServiceBound() {
decryptAllMessages();
}
private void executeApi(Message message) {
synchronized (message) {
Intent params = new Intent();
params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
if (message.getType() == Message.TYPE_TEXT) {
InputStream is = new ByteArrayInputStream(message.getBody().getBytes());
final OutputStream os = new ByteArrayOutputStream();
Intent result = getOpenPgpApi().executeApi(params, is, os);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
try {
os.flush();
final String body = os.toString();
if (body == null) {
throw new IOException("body was null");
}
message.setBody(body);
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
if (message.trusted()
&& message.treatAsDownloadable() != Message.Decision.NEVER
&& manager.getAutoAcceptFileSize() > 0) {
manager.createNewDownloadConnection(message);
}
} catch (IOException e) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
}
mXmppConnectionService.updateMessage(message);
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
synchronized (PgpDecryptionService.this) {
PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
messages.addFirst(message);
currentMessage = null;
storePendingIntent(pendingIntent);
}
break;
case OpenPgpApi.RESULT_CODE_ERROR:
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
mXmppConnectionService.updateMessage(message);
break;
}
} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
try {
final DownloadableFile inputFile = mXmppConnectionService.getFileBackend().getFile(message, false);
final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
outputFile.getParentFile().mkdirs();
outputFile.createNewFile();
InputStream is = new FileInputStream(inputFile);
OutputStream os = new FileOutputStream(outputFile);
Intent result = getOpenPgpApi().executeApi(params, is, os);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
URL url = message.getFileParams().url;
mXmppConnectionService.getFileBackend().updateFileParams(message, url);
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
inputFile.delete();
mXmppConnectionService.getFileBackend().updateMediaScanner(outputFile);
mXmppConnectionService.updateMessage(message);
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
synchronized (PgpDecryptionService.this) {
PendingIntent pendingIntent = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
messages.addFirst(message);
currentMessage = null;
storePendingIntent(pendingIntent);
}
break;
case OpenPgpApi.RESULT_CODE_ERROR:
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
mXmppConnectionService.updateMessage(message);
break;
}
} catch (final IOException e) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
mXmppConnectionService.updateMessage(message);
}
}
}
notifyIfPending(message);
}
public boolean isRunning() {
synchronized (keychainLockedLock) {
return !keychainLocked;
}
}
private synchronized void notifyIfPending(Message message) {
if (pendingNotifications.remove(message)) {
mXmppConnectionService.getNotificationService().push(message);
}
}
private void store(Message message) {
if (messages.containsKey(message.getConversation().getUuid())) {
messages.get(message.getConversation().getUuid()).add(message);
} else {
List<Message> messageList = Collections.synchronizedList(new LinkedList<Message>());
messageList.add(message);
messages.put(message.getConversation().getUuid(), messageList);
}
}
private void storePendingIntent(PendingIntent pendingIntent) {
this.pendingIntent = pendingIntent;
mXmppConnectionService.updateConversationUi();
}
private void decryptAllMessages() {
for (String uuid : messages.keySet()) {
decryptMessages(uuid);
}
}
public synchronized boolean hasPendingIntent(Conversation conversation) {
if (pendingIntent == null) {
return false;
} else {
for(Message message : messages) {
if (message.getConversation() == conversation) {
return true;
}
}
return false;
}
}
private void decryptMessages(final String uuid) {
synchronized (decryptingMessages) {
Boolean decrypting = decryptingMessages.get(uuid);
if ((decrypting != null && !decrypting) || decrypting == null) {
decryptingMessages.put(uuid, true);
decryptMessage(uuid);
}
}
}
public PendingIntent getPendingIntent() {
return pendingIntent;
}
private void decryptMessage(final String uuid) {
Message message = null;
synchronized (messages.get(uuid)) {
while (!messages.get(uuid).isEmpty()) {
if (messages.get(uuid).get(0).getEncryption() == Message.ENCRYPTION_PGP) {
if (isRunning()) {
message = messages.get(uuid).remove(0);
}
break;
} else {
messages.get(uuid).remove(0);
}
}
if (message != null && xmppConnectionService.getPgpEngine() != null) {
xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message message) {
messages.get(uuid).add(0, message);
decryptingMessages.put(uuid, false);
}
@Override
public void success(Message message) {
xmppConnectionService.updateConversationUi();
decryptMessage(uuid);
}
@Override
public void error(int error, Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
xmppConnectionService.updateConversationUi();
decryptMessage(uuid);
}
});
} else {
decryptingMessages.put(uuid, false);
}
}
}
private void decryptDirectly(final Message message) {
if (message.getEncryption() == Message.ENCRYPTION_PGP && xmppConnectionService.getPgpEngine() != null) {
xmppConnectionService.getPgpEngine().decrypt(message, new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message message) {
store(message);
}
@Override
public void success(Message message) {
xmppConnectionService.updateConversationUi();
xmppConnectionService.getNotificationService().updateNotification(false);
}
@Override
public void error(int error, Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
xmppConnectionService.updateConversationUi();
}
});
}
}
public boolean isConnected() {
return getOpenPgpApi() != null;
}
}

View file

@ -2,8 +2,9 @@ package eu.siacs.conversations.crypto;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
@ -15,15 +16,14 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
@ -37,99 +37,6 @@ public class PgpEngine {
this.mXmppConnectionService = service;
}
public void decrypt(final Message message,
final UiCallback<Message> callback) {
Intent params = new Intent();
params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
if (message.getType() == Message.TYPE_TEXT) {
InputStream is = new ByteArrayInputStream(message.getBody()
.getBytes());
final OutputStream os = new ByteArrayOutputStream();
api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
try {
os.flush();
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
message.setBody(os.toString());
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager();
if (message.trusted()
&& message.treatAsDownloadable() != Message.Decision.NEVER
&& manager.getAutoAcceptFileSize() > 0) {
manager.createNewDownloadConnection(message);
}
mXmppConnectionService.updateMessage(message);
callback.success(message);
}
} catch (IOException e) {
callback.error(R.string.openpgp_error, message);
return;
}
return;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequried((PendingIntent) result
.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
message);
return;
case OpenPgpApi.RESULT_CODE_ERROR:
callback.error(R.string.openpgp_error, message);
}
}
});
} else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
try {
final DownloadableFile inputFile = this.mXmppConnectionService
.getFileBackend().getFile(message, false);
final DownloadableFile outputFile = this.mXmppConnectionService
.getFileBackend().getFile(message, true);
outputFile.getParentFile().mkdirs();
outputFile.createNewFile();
InputStream is = new FileInputStream(inputFile);
OutputStream os = new FileOutputStream(outputFile);
api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
URL url = message.getFileParams().url;
mXmppConnectionService.getFileBackend().updateFileParams(message,url);
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
PgpEngine.this.mXmppConnectionService
.updateMessage(message);
inputFile.delete();
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(outputFile));
mXmppConnectionService.sendBroadcast(intent);
callback.success(message);
return;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
callback.userInputRequried(
(PendingIntent) result
.getParcelableExtra(OpenPgpApi.RESULT_INTENT),
message);
return;
case OpenPgpApi.RESULT_CODE_ERROR:
callback.error(R.string.openpgp_error, message);
}
}
});
} catch (final IOException e) {
callback.error(R.string.error_decrypting_file, message);
}
}
}
public void encrypt(final Message message, final UiCallback<Message> callback) {
Intent params = new Intent();
params.setAction(OpenPgpApi.ACTION_ENCRYPT);
@ -158,7 +65,6 @@ public class PgpEngine {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
@ -185,6 +91,7 @@ public class PgpEngine {
message);
break;
case OpenPgpApi.RESULT_CODE_ERROR:
logError(conversation.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
callback.error(R.string.openpgp_error, message);
break;
}
@ -204,7 +111,6 @@ public class PgpEngine {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_ENCRYPT, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
@ -223,6 +129,7 @@ public class PgpEngine {
message);
break;
case OpenPgpApi.RESULT_CODE_ERROR:
logError(conversation.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
callback.error(R.string.openpgp_error, message);
break;
}
@ -259,7 +166,6 @@ public class PgpEngine {
InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
ByteArrayOutputStream os = new ByteArrayOutputStream();
Intent result = api.executeApi(params, is, os);
notifyPgpDecryptionService(account, OpenPgpApi.ACTION_DECRYPT_VERIFY, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
@ -273,6 +179,7 @@ public class PgpEngine {
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
return 0;
case OpenPgpApi.RESULT_CODE_ERROR:
logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
return 0;
}
return 0;
@ -295,6 +202,7 @@ public class PgpEngine {
account);
return;
case OpenPgpApi.RESULT_CODE_ERROR:
logError(account, (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
callback.error(R.string.openpgp_error, account);
}
}
@ -303,7 +211,7 @@ public class PgpEngine {
public void generateSignature(final Account account, String status,
final UiCallback<Account> callback) {
if (account.getPgpId() == -1) {
if (account.getPgpId() == 0) {
return;
}
Intent params = new Intent();
@ -312,11 +220,11 @@ public class PgpEngine {
params.putExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, account.getPgpId());
InputStream is = new ByteArrayInputStream(status.getBytes());
final OutputStream os = new ByteArrayOutputStream();
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": signing status message \""+status+"\"");
api.executeApiAsync(params, is, os, new IOpenPgpCallback() {
@Override
public void onReturn(Intent result) {
notifyPgpDecryptionService(account, OpenPgpApi.ACTION_SIGN, result);
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
StringBuilder signatureBuilder = new StringBuilder();
@ -351,7 +259,13 @@ public class PgpEngine {
account);
return;
case OpenPgpApi.RESULT_CODE_ERROR:
callback.error(R.string.openpgp_error, account);
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
if (error != null && "signing subkey not found!".equals(error.getMessage())) {
callback.error(0,account);
} else {
logError(account, error);
callback.error(R.string.unable_to_connect_to_keychain, null);
}
}
}
});
@ -375,40 +289,30 @@ public class PgpEngine {
contact);
return;
case OpenPgpApi.RESULT_CODE_ERROR:
logError(contact.getAccount(), (OpenPgpError) result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
callback.error(R.string.openpgp_error, contact);
}
}
});
}
public PendingIntent getIntentForKey(Contact contact) {
Intent params = new Intent();
params.setAction(OpenPgpApi.ACTION_GET_KEY);
params.putExtra(OpenPgpApi.EXTRA_KEY_ID, contact.getPgpKeyId());
Intent result = api.executeApi(params, null, null);
return (PendingIntent) result
.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
private static void logError(Account account, OpenPgpError error) {
if (error != null) {
Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": OpenKeychain error '"+error.getMessage()+"' code="+error.getErrorId());
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": OpenKeychain error with no message");
}
}
public PendingIntent getIntentForKey(Account account, long pgpKeyId) {
public PendingIntent getIntentForKey(Contact contact) {
return getIntentForKey(contact.getPgpKeyId());
}
public PendingIntent getIntentForKey(long pgpKeyId) {
Intent params = new Intent();
params.setAction(OpenPgpApi.ACTION_GET_KEY);
params.putExtra(OpenPgpApi.EXTRA_KEY_ID, pgpKeyId);
Intent result = api.executeApi(params, null, null);
return (PendingIntent) result
.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
}
private void notifyPgpDecryptionService(Account account, String action, final Intent result) {
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, 0)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
if (OpenPgpApi.ACTION_SIGN.equals(action)) {
account.getPgpDecryptionService().onKeychainUnlocked();
}
break;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
account.getPgpDecryptionService().onKeychainLocked();
break;
}
return (PendingIntent) result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
}
}

View file

@ -1,5 +1,6 @@
package eu.siacs.conversations.crypto.axolotl;
import android.os.Bundle;
import android.security.KeyChain;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -24,13 +25,17 @@ import java.security.PrivateKey;
import java.security.Security;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
@ -39,6 +44,7 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
@ -51,6 +57,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
public static final String PEP_DEVICE_LIST_NOTIFY = PEP_DEVICE_LIST + "+notify";
public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
public static final String PEP_VERIFICATION = PEP_PREFIX + ".verification";
@ -70,28 +77,50 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private int numPublishTriesOnEmptyPep = 0;
private boolean pepBroken = false;
private AtomicBoolean ownPushPending = new AtomicBoolean(false);
@Override
public void onAdvancedStreamFeaturesAvailable(Account account) {
if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().pep()) {
if (Config.supportOmemo()
&& account.getXmppConnection() != null
&& account.getXmppConnection().getFeatures().pep()) {
publishBundlesIfNeeded(true, false);
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping OMEMO initialization");
}
}
public boolean fetchMapHasErrors(Contact contact) {
Jid jid = contact.getJid().toBareJid();
if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) {
AxolotlAddress address = new AxolotlAddress(jid.toString(), foreignId);
if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
return true;
public boolean fetchMapHasErrors(List<Jid> jids) {
for(Jid jid : jids) {
if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) {
AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId);
if (fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
return true;
}
}
}
}
return false;
}
public void preVerifyFingerprint(Contact contact, String fingerprint) {
axolotlStore.preVerifyFingerprint(contact.getAccount(), contact.getJid().toBareJid().toPreppedString(), fingerprint);
}
public void preVerifyFingerprint(Account account, String fingerprint) {
axolotlStore.preVerifyFingerprint(account, account.getJid().toBareJid().toPreppedString(), fingerprint);
}
public boolean hasVerifiedKeys(String name) {
for(XmppAxolotlSession session : this.sessions.getAll(new AxolotlAddress(name,0)).values()) {
if (session.getTrust().isVerified()) {
return true;
}
}
return false;
}
private static class AxolotlAddressMap<T> {
protected Map<String, Map<Integer, T>> map;
protected final Object MAP_LOCK = new Object();
@ -158,15 +187,28 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
for (Integer deviceId : deviceIds) {
AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
if(Config.X509_VERIFICATION) {
X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", ""));
if (certificate != null) {
Bundle information = CryptoHelper.extractCertificateInformation(certificate);
try {
final String cn = information.getString("subject_cn");
final Jid jid = Jid.fromString(bareJid);
Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
account.getRoster().getContact(jid).setCommonName(cn);
} catch (final InvalidJidException ignored) {
//ignored
}
}
}
this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
}
}
private void fillMap(SQLiteAxolotlStore store) {
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toPreppedString());
putDevicesForJid(account.getJid().toBareJid().toPreppedString(), deviceIds, store);
for (Contact contact : account.getRoster().getContacts()) {
Jid bareJid = contact.getJid().toBareJid();
String address = bareJid.toString();
@ -180,7 +222,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
public void put(AxolotlAddress address, XmppAxolotlSession value) {
super.put(address, value);
value.setNotFresh();
xmppConnectionService.syncRosterToDisk(account);
xmppConnectionService.syncRosterToDisk(account); //TODO why?
}
public void put(XmppAxolotlSession session) {
@ -193,11 +235,26 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
SUCCESS,
SUCCESS_VERIFIED,
TIMEOUT,
SUCCESS_TRUSTED,
ERROR
}
private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
public void clearErrorFor(Jid jid) {
synchronized (MAP_LOCK) {
Map<Integer, FetchStatus> devices = this.map.get(jid.toBareJid().toPreppedString());
if (devices == null) {
return;
}
for(Map.Entry<Integer, FetchStatus> entry : devices.entrySet()) {
if (entry.getValue() == FetchStatus.ERROR) {
Log.d(Config.LOGTAG,"resetting error for "+jid.toBareJid()+"("+entry.getKey()+")");
entry.setValue(FetchStatus.TIMEOUT);
}
}
}
}
}
public static String getLogprefix(Account account) {
@ -222,57 +279,80 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return axolotlStore.getIdentityKeyPair().getPublicKey().getFingerprint().replaceAll("\\s", "");
}
public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status) {
return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toPreppedString(), status);
}
public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) {
return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust);
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, Jid jid) {
return axolotlStore.getContactKeysWithTrust(jid.toBareJid().toPreppedString(), status);
}
public long getNumTrustedKeys(Contact contact) {
return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
public Set<IdentityKey> getKeysWithTrust(FingerprintStatus status, List<Jid> jids) {
Set<IdentityKey> keys = new HashSet<>();
for(Jid jid : jids) {
keys.addAll(axolotlStore.getContactKeysWithTrust(jid.toPreppedString(), status));
}
return keys;
}
public long getNumTrustedKeys(Jid jid) {
return axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString());
}
public boolean anyTargetHasNoTrustedKeys(List<Jid> jids) {
for(Jid jid : jids) {
if (axolotlStore.getContactNumTrustedKeys(jid.toBareJid().toPreppedString()) == 0) {
return true;
}
}
return false;
}
private AxolotlAddress getAddressForJid(Jid jid) {
return new AxolotlAddress(jid.toString(), 0);
return new AxolotlAddress(jid.toPreppedString(), 0);
}
private Set<XmppAxolotlSession> findOwnSessions() {
public Collection<XmppAxolotlSession> findOwnSessions() {
AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
return new HashSet<>(this.sessions.getAll(ownAddress).values());
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(ownAddress).values());
Collections.sort(s);
return s;
}
private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
return new HashSet<>(this.sessions.getAll(contactAddress).values());
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress).values());
Collections.sort(s);
return s;
}
public Set<String> getFingerprintsForOwnSessions() {
Set<String> fingerprints = new HashSet<>();
for (XmppAxolotlSession session : findOwnSessions()) {
fingerprints.add(session.getFingerprint());
private Set<XmppAxolotlSession> findSessionsForConversation(Conversation conversation) {
HashSet<XmppAxolotlSession> sessions = new HashSet<>();
for(Jid jid : conversation.getAcceptedCryptoTargets()) {
sessions.addAll(this.sessions.getAll(getAddressForJid(jid)).values());
}
return fingerprints;
return sessions;
}
public Set<String> getFingerprintsForContact(final Contact contact) {
Set<String> fingerprints = new HashSet<>();
for (XmppAxolotlSession session : findSessionsforContact(contact)) {
fingerprints.add(session.getFingerprint());
}
return fingerprints;
}
private boolean hasAny(Contact contact) {
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
return sessions.hasAny(contactAddress);
private boolean hasAny(Jid jid) {
return sessions.hasAny(getAddressForJid(jid));
}
public boolean isPepBroken() {
return this.pepBroken;
}
public void resetBrokenness() {
this.pepBroken = false;
numPublishTriesOnEmptyPep = 0;
}
public void clearErrorsInFetchStatusMap(Jid jid) {
fetchStatusMap.clearErrorFor(jid);
}
public void regenerateKeys(boolean wipeOther) {
axolotlStore.regenerate();
sessions.clear();
@ -284,62 +364,66 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return axolotlStore.getLocalRegistrationId();
}
public AxolotlAddress getOwnAxolotlAddress() {
return new AxolotlAddress(account.getJid().toBareJid().toPreppedString(),getOwnDeviceId());
}
public Set<Integer> getOwnDeviceIds() {
return this.deviceIds.get(account.getJid().toBareJid());
}
private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
final XmppAxolotlSession.Trust from,
final XmppAxolotlSession.Trust to) {
for (Integer deviceId : deviceIds) {
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null
&& session.getTrust() == from) {
session.setTrust(to);
}
}
}
public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
if (jid.toBareJid().equals(account.getJid().toBareJid())) {
if (!deviceIds.isEmpty()) {
Log.d(Config.LOGTAG, getLogprefix(account) + "Received non-empty own device list. Resetting publish attemps and pepBroken status.");
pepBroken = false;
numPublishTriesOnEmptyPep = 0;
}
if (deviceIds.contains(getOwnDeviceId())) {
deviceIds.remove(getOwnDeviceId());
} else {
publishOwnDeviceId(deviceIds);
}
for (Integer deviceId : deviceIds) {
AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
if (sessions.get(ownDeviceAddress) == null) {
buildSessionFromPEP(ownDeviceAddress);
boolean me = jid.toBareJid().equals(account.getJid().toBareJid());
if (me && ownPushPending.getAndSet(false)) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring own device update because of pending push");
return;
}
boolean needsPublishing = me && !deviceIds.contains(getOwnDeviceId());
if (me) {
deviceIds.remove(getOwnDeviceId());
}
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toPreppedString()));
expiredDevices.removeAll(deviceIds);
for (Integer deviceId : expiredDevices) {
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null) {
if (session.getTrust().isActive()) {
session.setTrust(session.getTrust().toInactive());
}
}
}
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
expiredDevices.removeAll(deviceIds);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED_X509,
XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
Set<Integer> newDevices = new HashSet<>(deviceIds);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
XmppAxolotlSession.Trust.TRUSTED);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509,
XmppAxolotlSession.Trust.TRUSTED_X509);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
XmppAxolotlSession.Trust.UNDECIDED);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
XmppAxolotlSession.Trust.UNTRUSTED);
for (Integer deviceId : newDevices) {
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null) {
if (!session.getTrust().isActive()) {
Log.d(Config.LOGTAG,"reactivating device with fingerprint "+session.getFingerprint());
session.setTrust(session.getTrust().toActive());
}
}
}
if (me) {
if (Config.OMEMO_AUTO_EXPIRY != 0) {
needsPublishing |= deviceIds.removeAll(getExpiredDevices());
}
for (Integer deviceId : deviceIds) {
AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), deviceId);
if (sessions.get(ownDeviceAddress) == null) {
FetchStatus status = fetchStatusMap.get(ownDeviceAddress);
if (status == null || status == FetchStatus.TIMEOUT) {
fetchStatusMap.put(ownDeviceAddress, FetchStatus.PENDING);
this.buildSessionFromPEP(ownDeviceAddress);
}
}
}
if (needsPublishing) {
publishOwnDeviceId(deviceIds);
}
}
this.deviceIds.put(jid, deviceIds);
mXmppConnectionService.updateConversationUi(); //update the lock icon
mXmppConnectionService.keyStatusUpdated(null);
}
@ -352,16 +436,13 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
deviceIds.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
// TODO: implement this!
}
});
mXmppConnectionService.sendIqPacket(account, publish, null);
}
public void purgeKey(final String fingerprint) {
axolotlStore.setFingerprintTrust(fingerprint.replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
public void distrustFingerprint(final String fingerprint) {
final String fp = fingerprint.replaceAll("\\s", "");
final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp);
axolotlStore.setFingerprintStatus(fp,fingerprintStatus.toUntrusted());
}
public void publishOwnDeviceIdIfNeeded() {
@ -378,41 +459,62 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
} else {
Element item = mXmppConnectionService.getIqParser().getItem(packet);
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
if (!deviceIds.contains(getOwnDeviceId())) {
publishOwnDeviceId(deviceIds);
}
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": retrieved own device list: "+deviceIds);
registerDevices(account.getJid().toBareJid(),deviceIds);
}
}
});
}
public void publishOwnDeviceId(Set<Integer> deviceIds) {
Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
if (!deviceIdsCopy.contains(getOwnDeviceId())) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist.");
if (deviceIdsCopy.isEmpty()) {
if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
pepBroken = true;
return;
} else {
numPublishTriesOnEmptyPep++;
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
}
} else {
numPublishTriesOnEmptyPep = 0;
}
deviceIdsCopy.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() != IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
private Set<Integer> getExpiredDevices() {
Set<Integer> devices = new HashSet<>();
for(XmppAxolotlSession session : findOwnSessions()) {
if (session.getTrust().isActive()) {
long diff = System.currentTimeMillis() - session.getTrust().getLastActivation();
if (diff > Config.OMEMO_AUTO_EXPIRY) {
long lastMessageDiff = System.currentTimeMillis() - mXmppConnectionService.databaseBackend.getLastTimeFingerprintUsed(account,session.getFingerprint());
long hours = Math.round(lastMessageDiff/(1000*60.0*60.0));
if (lastMessageDiff > Config.OMEMO_AUTO_EXPIRY) {
devices.add(session.getRemoteAddress().getDeviceId());
session.setTrust(session.getTrust().toInactive());
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": added own device " + session.getFingerprint() + " to list of expired devices. Last message received "+hours+" hours ago");
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": own device "+session.getFingerprint()+" was active "+hours+" hours ago");
}
}
});
}
}
return devices;
}
public void publishOwnDeviceId(Set<Integer> deviceIds) {
Set<Integer> deviceIdsCopy = new HashSet<>(deviceIds);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "publishing own device ids");
if (deviceIdsCopy.isEmpty()) {
if (numPublishTriesOnEmptyPep >= publishTriesThreshold) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device publish attempt threshold exceeded, aborting...");
pepBroken = true;
return;
} else {
numPublishTriesOnEmptyPep++;
Log.w(Config.LOGTAG, getLogprefix(account) + "Own device list empty, attempting to publish (try " + numPublishTriesOnEmptyPep + ")");
}
} else {
numPublishTriesOnEmptyPep = 0;
}
deviceIdsCopy.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIdsCopy);
ownPushPending.set(true);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
ownPushPending.set(false);
if (packet.getType() == IqPacket.TYPE.ERROR) {
pepBroken = true;
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing own device id" + packet.findChild("error"));
}
}
});
}
public void publishDeviceVerificationAndBundle(final SignedPreKeyRecord signedPreKeyRecord,
@ -571,29 +673,69 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
Log.d(Config.LOGTAG, getLogprefix(account) + "Announcing device " + getOwnDeviceId());
publishOwnDeviceIdIfNeeded();
}
} else {
} else if (packet.getType() == IqPacket.TYPE.ERROR) {
pepBroken = true;
Log.d(Config.LOGTAG, getLogprefix(account) + "Error received while publishing bundle: " + packet.findChild("error"));
}
}
});
}
public boolean isContactAxolotlCapable(Contact contact) {
Jid jid = contact.getJid().toBareJid();
return hasAny(contact) ||
(deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
public enum AxolotlCapability {
FULL,
MISSING_PRESENCE,
MISSING_KEYS,
WRONG_CONFIGURATION,
NO_MEMBERS
}
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
return axolotlStore.getFingerprintTrust(fingerprint);
public boolean isConversationAxolotlCapable(Conversation conversation) {
return isConversationAxolotlCapableDetailed(conversation).first == AxolotlCapability.FULL;
}
public Pair<AxolotlCapability,Jid> isConversationAxolotlCapableDetailed(Conversation conversation) {
if (conversation.getMode() == Conversation.MODE_SINGLE
|| (conversation.getMucOptions().membersOnly() && conversation.getMucOptions().nonanonymous())) {
final List<Jid> jids = getCryptoTargets(conversation);
for(Jid jid : jids) {
if (!hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty())) {
if (conversation.getAccount().getRoster().getContact(jid).mutualPresenceSubscription()) {
return new Pair<>(AxolotlCapability.MISSING_KEYS,jid);
} else {
return new Pair<>(AxolotlCapability.MISSING_PRESENCE,jid);
}
}
}
if (jids.size() > 0) {
return new Pair<>(AxolotlCapability.FULL, null);
} else {
return new Pair<>(AxolotlCapability.NO_MEMBERS, null);
}
} else {
return new Pair<>(AxolotlCapability.WRONG_CONFIGURATION, null);
}
}
public List<Jid> getCryptoTargets(Conversation conversation) {
final List<Jid> jids;
if (conversation.getMode() == Conversation.MODE_SINGLE) {
jids = Arrays.asList(conversation.getJid().toBareJid());
} else {
jids = conversation.getMucOptions().getMembers();
}
return jids;
}
public FingerprintStatus getFingerprintTrust(String fingerprint) {
return axolotlStore.getFingerprintStatus(fingerprint);
}
public X509Certificate getFingerprintCertificate(String fingerprint) {
return axolotlStore.getFingerprintCertificate(fingerprint);
}
public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
axolotlStore.setFingerprintTrust(fingerprint, trust);
public void setFingerprintTrust(String fingerprint, FingerprintStatus status) {
axolotlStore.setFingerprintStatus(fingerprint, status);
}
private void verifySessionWithPEP(final XmppAxolotlSession session) {
@ -616,9 +758,18 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
mXmppConnectionService.getMemorizingTrustManager().getNonInteractive().checkClientTrusted(verification.first, "RSA");
String fingerprint = session.getFingerprint();
Log.d(Config.LOGTAG, "verified session with x.509 signature. fingerprint was: "+fingerprint);
setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509);
setFingerprintTrust(fingerprint, FingerprintStatus.createActiveVerified(true));
axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]);
fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED);
Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]);
try {
final String cn = information.getString("subject_cn");
final Jid jid = Jid.fromString(address.getName());
Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn);
account.getRoster().getContact(jid).setCommonName(cn);
} catch (final InvalidJidException ignored) {
//ignored
}
finishBuildingSessionsFromPEP(address);
return;
} catch (Exception e) {
@ -641,25 +792,44 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
}
private final Set<Integer> PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>();
private void finishBuildingSessionsFromPEP(final AxolotlAddress address) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0);
Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress);
Map<Integer, FetchStatus> remote = fetchStatusMap.getAll(address);
if (!own.containsValue(FetchStatus.PENDING) && !remote.containsValue(FetchStatus.PENDING)) {
FetchStatus report = null;
if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.SUCCESS_VERIFIED)
| fetchStatusMap.getAll(address).containsValue(FetchStatus.SUCCESS_VERIFIED)) {
if (own.containsValue(FetchStatus.SUCCESS) || remote.containsValue(FetchStatus.SUCCESS)) {
report = FetchStatus.SUCCESS;
} else if (own.containsValue(FetchStatus.SUCCESS_VERIFIED) || remote.containsValue(FetchStatus.SUCCESS_VERIFIED)) {
report = FetchStatus.SUCCESS_VERIFIED;
} else if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.ERROR)
|| fetchStatusMap.getAll(address).containsValue(FetchStatus.ERROR)) {
} else if (own.containsValue(FetchStatus.SUCCESS_TRUSTED) || remote.containsValue(FetchStatus.SUCCESS_TRUSTED)) {
report = FetchStatus.SUCCESS_TRUSTED;
} else if (own.containsValue(FetchStatus.ERROR) || remote.containsValue(FetchStatus.ERROR)) {
report = FetchStatus.ERROR;
}
mXmppConnectionService.keyStatusUpdated(report);
}
if (Config.REMOVE_BROKEN_DEVICES) {
Set<Integer> ownDeviceIds = new HashSet<>(getOwnDeviceIds());
boolean publish = false;
for (Map.Entry<Integer, FetchStatus> entry : own.entrySet()) {
int id = entry.getKey();
if (entry.getValue() == FetchStatus.ERROR && PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT.add(id) && ownDeviceIds.remove(id)) {
publish = true;
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": error fetching own device with id " + id + ". removing from announcement");
}
}
if (publish) {
publishOwnDeviceId(ownDeviceIds);
}
}
}
private void buildSessionFromPEP(final AxolotlAddress address) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.toString());
if (address.getDeviceId() == getOwnDeviceId()) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new session for " + address.toString());
if (address.equals(getOwnAxolotlAddress())) {
throw new AssertionError("We should NEVER build a session with ourselves. What happened here?!");
}
@ -706,7 +876,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
if (Config.X509_VERIFICATION) {
verifySessionWithPEP(session);
} else {
fetchStatusMap.put(address, FetchStatus.SUCCESS);
FingerprintStatus status = getFingerprintTrust(bundle.getIdentityKey().getFingerprint().replaceAll("\\s",""));
FetchStatus fetchStatus;
if (status != null && status.isVerified()) {
fetchStatus = FetchStatus.SUCCESS_VERIFIED;
} else if (status != null && status.isTrusted()) {
fetchStatus = FetchStatus.SUCCESS_TRUSTED;
} else {
fetchStatus = FetchStatus.SUCCESS;
}
fetchStatusMap.put(address, fetchStatus);
finishBuildingSessionsFromPEP(address);
}
} catch (UntrustedIdentityException | InvalidKeyException e) {
@ -728,37 +907,35 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
return findDevicesWithoutSession(conversation.getContact().getJid().toBareJid());
}
public Set<AxolotlAddress> findDevicesWithoutSession(final Jid contactJid) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + contactJid);
Set<AxolotlAddress> addresses = new HashSet<>();
if (deviceIds.get(contactJid) != null) {
for (Integer foreignId : this.deviceIds.get(contactJid)) {
AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
sessions.put(address, session);
} else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + contactJid + ":" + foreignId);
if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
addresses.add(address);
for(Jid jid : getCryptoTargets(conversation)) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + jid);
if (deviceIds.get(jid) != null) {
for (Integer foreignId : this.deviceIds.get(jid)) {
AxolotlAddress address = new AxolotlAddress(jid.toPreppedString(), foreignId);
if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey);
sessions.put(address, session);
} else {
Log.d(Config.LOGTAG,getLogprefix(account)+"skipping over "+address+" because it's broken");
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + jid + ":" + foreignId);
if (fetchStatusMap.get(address) != FetchStatus.ERROR) {
addresses.add(address);
} else {
Log.d(Config.LOGTAG, getLogprefix(account) + "skipping over " + address + " because it's broken");
}
}
}
}
} else {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
}
} else {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
}
if (deviceIds.get(account.getJid().toBareJid()) != null) {
for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownId);
if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) {
@ -802,12 +979,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
public boolean trustedSessionVerified(final Conversation conversation) {
Set<XmppAxolotlSession> sessions = findSessionsforContact(conversation.getContact());
Set<XmppAxolotlSession> sessions = findSessionsForConversation(conversation);
sessions.addAll(findOwnSessions());
boolean verified = false;
for(XmppAxolotlSession session : sessions) {
if (session.getTrust().trusted()) {
if (session.getTrust() == XmppAxolotlSession.Trust.TRUSTED_X509) {
if (session.getTrust().isTrustedAndActive()) {
if (session.getTrust().getTrust() == FingerprintStatus.Trust.VERIFIED_X509) {
verified = true;
} else {
return false;
@ -817,55 +994,54 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
return verified;
}
public boolean hasPendingKeyFetches(Account account, Contact contact) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
AxolotlAddress foreignAddress = new AxolotlAddress(contact.getJid().toBareJid().toString(), 0);
return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
|| fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
public boolean hasPendingKeyFetches(Account account, List<Jid> jids) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), 0);
if (fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)) {
return true;
}
for(Jid jid : jids) {
AxolotlAddress foreignAddress = new AxolotlAddress(jid.toBareJid().toPreppedString(), 0);
if (fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING)) {
return true;
}
}
return false;
}
@Nullable
private XmppAxolotlMessage buildHeader(Contact contact) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
contact.getJid().toBareJid(), getOwnDeviceId());
Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
Set<XmppAxolotlSession> ownSessions = findOwnSessions();
if (contactSessions.isEmpty()) {
return null;
private boolean buildHeader(XmppAxolotlMessage axolotlMessage, Conversation conversation) {
Set<XmppAxolotlSession> remoteSessions = findSessionsForConversation(conversation);
Collection<XmppAxolotlSession> ownSessions = findOwnSessions();
if (remoteSessions.isEmpty()) {
return false;
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
for (XmppAxolotlSession session : contactSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
for (XmppAxolotlSession session : remoteSessions) {
axolotlMessage.addDevice(session);
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
for (XmppAxolotlSession session : ownSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session);
}
return axolotlMessage;
return true;
}
@Nullable
public XmppAxolotlMessage encrypt(Message message) {
XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
if (axolotlMessage != null) {
final String content;
if (message.hasFileOnRemoteHost()) {
content = message.getFileParams().url.toString();
} else {
content = message.getBody();
}
try {
axolotlMessage.encrypt(content);
} catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
return null;
}
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
final String content;
if (message.hasFileOnRemoteHost()) {
content = message.getFileParams().url.toString();
} else {
content = message.getBody();
}
try {
axolotlMessage.encrypt(content);
} catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
return null;
}
if (!buildHeader(axolotlMessage,message.getConversation())) {
return null;
}
return axolotlMessage;
@ -888,12 +1064,16 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
});
}
public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
public void prepareKeyTransportMessage(final Conversation conversation, final OnMessageCreatedCallback onMessageCreatedCallback) {
executor.execute(new Runnable() {
@Override
public void run() {
XmppAxolotlMessage axolotlMessage = buildHeader(contact);
onMessageCreatedCallback.run(axolotlMessage);
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().toBareJid(), getOwnDeviceId());
if (buildHeader(axolotlMessage,conversation)) {
onMessageCreatedCallback.run(axolotlMessage);
} else {
onMessageCreatedCallback.run(null);
}
}
});
}
@ -917,7 +1097,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
}
private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toPreppedString(),
message.getSenderDeviceId());
XmppAxolotlSession session = sessions.get(senderAddress);
if (session == null) {
@ -942,7 +1122,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
session.resetPreKeyId();
}
} catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from "+message.getFrom()+": " + e.getMessage());
}
if (session.isFresh() && plaintextMessage != null) {
@ -956,7 +1136,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
XmppAxolotlSession session = getReceivingSession(message);
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
try {
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
} catch (CryptoFailedException e) {
Log.d(Config.LOGTAG,"could not decrypt keyTransport message "+e.getMessage());
keyTransportMessage = null;
}
if (session.isFresh() && keyTransportMessage != null) {
putFreshSession(session);

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);
}
};
@ -90,16 +96,18 @@ public class SQLiteAxolotlStore implements AxolotlStore {
// --------------------------------------
private IdentityKeyPair loadIdentityKeyPair() {
IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account);
synchronized (mXmppConnectionService) {
IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account);
if (ownKey != null) {
if (ownKey != null) {
return ownKey;
} else {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve own IdentityKeyPair");
ownKey = generateIdentityKeyPair();
mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey);
}
return ownKey;
} else {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve own IdentityKeyPair");
ownKey = generateIdentityKeyPair();
mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownKey);
}
return ownKey;
}
private int loadRegistrationId() {
@ -125,15 +133,15 @@ public class SQLiteAxolotlStore implements AxolotlStore {
}
private int loadCurrentPreKeyId() {
String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
int reg_id;
if (regIdString != null) {
reg_id = Integer.valueOf(regIdString);
String prekeyIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
int prekey_id;
if (prekeyIdString != null) {
prekey_id = Integer.valueOf(prekeyIdString);
} else {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid());
reg_id = 0;
prekey_id = 0;
}
return reg_id;
return prekey_id;
}
public void regenerate() {
@ -183,7 +191,20 @@ public class SQLiteAxolotlStore implements AxolotlStore {
@Override
public void saveIdentity(String name, IdentityKey identityKey) {
if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
FingerprintStatus status = getFingerprintStatus(fingerprint);
if (status == null) {
if (mXmppConnectionService.blindTrustBeforeVerification() && !account.getAxolotlService().hasVerifiedKeys(name)) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": blindly trusted "+fingerprint+" of "+name);
status = FingerprintStatus.createActiveTrusted();
} else {
status = FingerprintStatus.createActiveUndecided();
}
} else {
status = status.toActive();
}
mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey, status);
trustCache.remove(fingerprint);
}
}
@ -206,12 +227,12 @@ public class SQLiteAxolotlStore implements AxolotlStore {
return true;
}
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
public FingerprintStatus getFingerprintStatus(String fingerprint) {
return (fingerprint == null)? null : trustCache.get(fingerprint);
}
public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
public void setFingerprintStatus(String fingerprint, FingerprintStatus status) {
mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, status);
trustCache.remove(fingerprint);
}
@ -223,8 +244,8 @@ public class SQLiteAxolotlStore implements AxolotlStore {
return mXmppConnectionService.databaseBackend.getIdentityKeyCertifcate(account, fingerprint);
}
public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) {
return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
public Set<IdentityKey> getContactKeysWithTrust(String bareJid, FingerprintStatus status) {
return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, status);
}
public long getContactNumTrustedKeys(String bareJid) {
@ -426,4 +447,8 @@ public class SQLiteAxolotlStore implements AxolotlStore {
public void removeSignedPreKey(int signedPreKeyId) {
mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
}
public void preVerifyFingerprint(Account account, String name, String fingerprint) {
mXmppConnectionService.databaseBackend.storePreVerification(account,name,fingerprint,FingerprintStatus.createInactiveVerified());
}
}

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;
@ -91,7 +93,11 @@ public class XmppAxolotlMessage {
private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
this.from = from;
Element header = axolotlMessage.findChild(HEADER);
this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
try {
this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid source id");
}
List<Element> keyElements = header.getChildren();
this.keys = new HashMap<>(keyElements.size());
for (Element keyElement : keyElements) {
@ -99,17 +105,18 @@ public class XmppAxolotlMessage {
case KEYTAG:
try {
Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
byte[] key = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
this.keys.put(recipientId, key);
byte[] key = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
boolean isPreKey =keyElement.getAttributeAsBoolean("prekey");
this.keys.put(recipientId, new XmppAxolotlSession.AxolotlKey(key,isPreKey));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
throw new IllegalArgumentException("invalid remote id");
}
break;
case IVTAG:
if (this.iv != null) {
throw new IllegalArgumentException("Duplicate iv entry");
}
iv = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
iv = Base64.decode(keyElement.getContent().trim(), Base64.DEFAULT);
break;
default:
Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString());
@ -118,7 +125,7 @@ public class XmppAxolotlMessage {
}
Element payloadElement = axolotlMessage.findChild(PAYLOAD);
if (payloadElement != null) {
ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT);
ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT);
}
}
@ -158,8 +165,15 @@ public class XmppAxolotlMessage {
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
this.innerKey = secretKey.getEncoded();
this.ciphertext = cipher.doFinal(plaintext.getBytes());
this.ciphertext = cipher.doFinal(Config.OMEMO_PADDING ? getPaddedBytes(plaintext) : plaintext.getBytes());
if (Config.PUT_AUTH_TAG_INTO_KEY && this.ciphertext != null) {
this.authtagPlusInnerKey = new byte[16+16];
byte[] ciphertext = new byte[this.ciphertext.length - 16];
System.arraycopy(this.ciphertext,0,ciphertext,0,ciphertext.length);
System.arraycopy(this.ciphertext,ciphertext.length,authtagPlusInnerKey,16,16);
System.arraycopy(this.innerKey,0,authtagPlusInnerKey,0,this.innerKey.length);
this.ciphertext = ciphertext;
}
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
| InvalidAlgorithmParameterException e) {
@ -167,6 +181,22 @@ public class XmppAxolotlMessage {
}
}
private static byte[] getPaddedBytes(String plaintext) {
int plainLength = plaintext.getBytes().length;
int pad = Math.max(64,(plainLength / 32 + 1) * 32) - plainLength;
SecureRandom random = new SecureRandom();
int left = random.nextInt(pad);
int right = pad - left;
StringBuilder builder = new StringBuilder(plaintext);
for(int i = 0; i < left; ++i) {
builder.insert(0,random.nextBoolean() ? "\t" : " ");
}
for(int i = 0; i < right; ++i) {
builder.append(random.nextBoolean() ? "\t" : " ");
}
return builder.toString().getBytes();
}
public Jid getFrom() {
return this.from;
}
@ -180,7 +210,12 @@ public class XmppAxolotlMessage {
}
public void addDevice(XmppAxolotlSession session) {
byte[] key = session.processSending(innerKey);
XmppAxolotlSession.AxolotlKey key;
if (authtagPlusInnerKey != null) {
key = session.processSending(authtagPlusInnerKey);
} else {
key = session.processSending(innerKey);
}
if (key != null) {
keys.put(session.getRemoteAddress().getDeviceId(), key);
}
@ -198,30 +233,33 @@ public class XmppAxolotlMessage {
Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX);
Element headerElement = encryptionElement.addChild(HEADER);
headerElement.setAttribute(SOURCEID, sourceDeviceId);
for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) {
for (Map.Entry<Integer, XmppAxolotlSession.AxolotlKey> keyEntry : keys.entrySet()) {
Element keyElement = new Element(KEYTAG);
keyElement.setAttribute(REMOTEID, keyEntry.getKey());
keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT));
if (keyEntry.getValue().prekey) {
keyElement.setAttribute("prekey","true");
}
keyElement.setContent(Base64.encodeToString(keyEntry.getValue().key, Base64.NO_WRAP));
headerElement.addChild(keyElement);
}
headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.NO_WRAP));
if (ciphertext != null) {
Element payload = encryptionElement.addChild(PAYLOAD);
payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
payload.setContent(Base64.encodeToString(ciphertext, Base64.NO_WRAP));
}
return encryptionElement;
}
private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) {
byte[] encryptedKey = keys.get(sourceDeviceId);
return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null;
private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
XmppAxolotlSession.AxolotlKey encryptedKey = keys.get(sourceDeviceId);
if (encryptedKey == null) {
throw new CryptoFailedException("Message was not encrypted for this device");
}
return session.processReceiving(encryptedKey);
}
public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) {
byte[] key = unpackKey(session, sourceDeviceId);
return (key != null)
? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV())
: null;
public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV());
}
public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
@ -229,6 +267,19 @@ public class XmppAxolotlMessage {
byte[] key = unpackKey(session, sourceDeviceId);
if (key != null) {
try {
if (key.length >= 32) {
int authtaglength = key.length - 16;
Log.d(Config.LOGTAG,"found auth tag as part of omemo key");
byte[] newCipherText = new byte[key.length - 16 + ciphertext.length];
byte[] newKey = new byte[16];
System.arraycopy(ciphertext, 0, newCipherText, 0, ciphertext.length);
System.arraycopy(key, 16, newCipherText, ciphertext.length, authtaglength);
System.arraycopy(key,0,newKey,0,newKey.length);
ciphertext = newCipherText;
key = newKey;
}
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
@ -236,7 +287,7 @@ public class XmppAxolotlMessage {
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
String plaintext = new String(cipher.doFinal(ciphertext));
plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint());
plaintextMessage = new XmppAxolotlPlaintextMessage(Config.OMEMO_PADDING ? plaintext.trim() : plaintext, session.getFingerprint());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException

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,77 +75,86 @@ public class XmppAxolotlSession {
this.fresh = false;
}
protected void setTrust(Trust trust) {
sqLiteAxolotlStore.setFingerprintTrust(getFingerprint(), trust);
protected void setTrust(FingerprintStatus status) {
sqLiteAxolotlStore.setFingerprintStatus(getFingerprint(), status);
}
protected Trust getTrust() {
Trust trust = sqLiteAxolotlStore.getFingerprintTrust(getFingerprint());
return (trust == null) ? Trust.UNDECIDED : trust;
public FingerprintStatus getTrust() {
FingerprintStatus status = sqLiteAxolotlStore.getFingerprintStatus(getFingerprint());
return (status == null) ? FingerprintStatus.createActiveUndecided() : status;
}
@Nullable
public byte[] processReceiving(byte[] encryptedKey) {
byte[] plaintext = null;
Trust trust = getTrust();
switch (trust) {
case INACTIVE_TRUSTED:
case UNDECIDED:
case UNTRUSTED:
case TRUSTED:
case INACTIVE_TRUSTED_X509:
case TRUSTED_X509:
public byte[] processReceiving(AxolotlKey encryptedKey) throws CryptoFailedException {
byte[] plaintext;
FingerprintStatus status = getTrust();
if (!status.isCompromised()) {
try {
CiphertextMessage ciphertextMessage;
try {
try {
PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
IdentityKey msgIdentityKey = message.getIdentityKey();
if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint());
} else {
this.identityKey = msgIdentityKey;
plaintext = cipher.decrypt(message);
if (message.getPreKeyId().isPresent()) {
preKeyId = message.getPreKeyId().get();
}
}
} catch (InvalidMessageException | InvalidVersionException e) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received");
WhisperMessage message = new WhisperMessage(encryptedKey);
plaintext = cipher.decrypt(message);
} catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
ciphertextMessage = new PreKeyWhisperMessage(encryptedKey.key);
Optional<Integer> optionalPreKeyId = ((PreKeyWhisperMessage) ciphertextMessage).getPreKeyId();
IdentityKey identityKey = ((PreKeyWhisperMessage) ciphertextMessage).getIdentityKey();
if (!optionalPreKeyId.isPresent()) {
throw new CryptoFailedException("PreKeyWhisperMessage did not contain a PreKeyId");
}
} catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
}
if (plaintext != null) {
if (trust == Trust.INACTIVE_TRUSTED) {
setTrust(Trust.TRUSTED);
} else if (trust == Trust.INACTIVE_TRUSTED_X509) {
setTrust(Trust.TRUSTED_X509);
preKeyId = optionalPreKeyId.get();
if (this.identityKey != null && !this.identityKey.equals(identityKey)) {
throw new CryptoFailedException("Received PreKeyWhisperMessage but preexisting identity key changed.");
}
this.identityKey = identityKey;
} catch (InvalidVersionException | InvalidMessageException e) {
ciphertextMessage = new WhisperMessage(encryptedKey.key);
}
break;
case COMPROMISED:
default:
// ignore
break;
if (ciphertextMessage instanceof PreKeyWhisperMessage) {
plaintext = cipher.decrypt((PreKeyWhisperMessage) ciphertextMessage);
} else {
plaintext = cipher.decrypt((WhisperMessage) ciphertextMessage);
}
} catch (InvalidKeyException | LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException | InvalidKeyIdException | UntrustedIdentityException e) {
if (!(e instanceof DuplicateMessageException)) {
e.printStackTrace();
}
throw new CryptoFailedException("Error decrypting WhisperMessage " + e.getClass().getSimpleName() + ": " + e.getMessage());
}
if (!status.isActive()) {
setTrust(status.toActive());
}
} else {
throw new CryptoFailedException("not encrypting omemo message from fingerprint "+getFingerprint()+" because it was marked as compromised");
}
return plaintext;
}
@Nullable
public byte[] processSending(@NonNull byte[] outgoingMessage) {
Trust trust = getTrust();
if (trust.trusted()) {
public AxolotlKey processSending(@NonNull byte[] outgoingMessage) {
FingerprintStatus status = getTrust();
if (status.isTrustedAndActive()) {
CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
return ciphertextMessage.serialize();
return new AxolotlKey(ciphertextMessage.serialize(),ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE);
} else {
return null;
}
}
public Account getAccount() {
return account;
}
@Override
public int compareTo(XmppAxolotlSession o) {
return getTrust().compareTo(o.getTrust());
}
public static class AxolotlKey {
public final byte[] key;
public final boolean prekey;
public AxolotlKey(byte[] key, boolean prekey) {
this.key = key;
this.prekey = prekey;
}
}
}

View file

@ -0,0 +1,28 @@
package eu.siacs.conversations.crypto.sasl;
import java.security.SecureRandom;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xml.TagWriter;
public class Anonymous extends SaslMechanism {
public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) {
super(tagWriter, account, rng);
}
@Override
public int getPriority() {
return 0;
}
@Override
public String getMechanism() {
return "ANONYMOUS";
}
@Override
public String getClientFirstMessage() {
return "";
}
}

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,152 +27,4 @@ public class ScramSha1 extends SaslMechanism {
public String getMechanism() {
return "SCRAM-SHA-1";
}
@Override
public String getClientFirstMessage() {
if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) {
clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) +
",r=" + this.clientNonce;
state = State.AUTH_TEXT_SENT;
}
return Base64.encodeToString(
(GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()),
Base64.NO_WRAP);
}
@Override
public String getResponse(final String challenge) throws AuthenticationException {
switch (state) {
case AUTH_TEXT_SENT:
if (challenge == null) {
throw new AuthenticationException("challenge can not be null");
}
byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT);
final Tokenizer tokenizer = new Tokenizer(serverFirstMessage);
String nonce = "";
int iterationCount = -1;
String salt = "";
for (final String token : tokenizer) {
if (token.charAt(1) == '=') {
switch (token.charAt(0)) {
case 'i':
try {
iterationCount = Integer.parseInt(token.substring(2));
} catch (final NumberFormatException e) {
throw new AuthenticationException(e);
}
break;
case 's':
salt = token.substring(2);
break;
case 'r':
nonce = token.substring(2);
break;
case 'm':
/*
* RFC 5802:
* m: This attribute is reserved for future extensibility. In this
* version of SCRAM, its presence in a client or a server message
* MUST cause authentication failure when the attribute is parsed by
* the other end.
*/
throw new AuthenticationException("Server sent reserved token: `m'");
}
}
}
if (iterationCount < 0) {
throw new AuthenticationException("Server did not send iteration count");
}
if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) {
throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce);
}
if (salt.isEmpty()) {
throw new AuthenticationException("Server sent empty salt");
}
final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString(
GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce;
final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ','
+ clientFinalMessageWithoutProof).getBytes();
// Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations".
final KeyPair keys = CACHE.get(
CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + ","
+ CryptoHelper.bytesToHex(account.getPassword().getBytes()) + ","
+ CryptoHelper.bytesToHex(salt.getBytes()) + ","
+ String.valueOf(iterationCount)
);
if (keys == null) {
throw new AuthenticationException("Invalid keys generated");
}
final byte[] clientSignature;
try {
serverSignature = hmac(keys.serverKey, authMessage);
final byte[] storedKey = digest(keys.clientKey);
clientSignature = hmac(storedKey, authMessage);
} catch (final InvalidKeyException e) {
throw new AuthenticationException(e);
}
final byte[] clientProof = new byte[keys.clientKey.length];
for (int i = 0; i < clientProof.length; i++) {
clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]);
}
final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" +
Base64.encodeToString(clientProof, Base64.NO_WRAP);
state = State.RESPONSE_SENT;
return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP);
case RESPONSE_SENT:
final String clientCalculatedServerFinalMessage = "v=" +
Base64.encodeToString(serverSignature, Base64.NO_WRAP);
if (challenge == null || !clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) {
throw new AuthenticationException("Server final message does not match calculated final message");
}
state = State.VALID_SERVER_RESPONSE;
return "";
default:
throw new InvalidStateException(state);
}
}
public static synchronized byte[] hmac(final byte[] key, final byte[] input)
throws InvalidKeyException {
HMAC.init(new KeyParameter(key));
HMAC.update(input, 0, input.length);
final byte[] out = new byte[HMAC.getMacSize()];
HMAC.doFinal(out, 0);
return out;
}
public static synchronized byte[] digest(byte[] bytes) {
DIGEST.reset();
DIGEST.update(bytes, 0, bytes.length);
final byte[] out = new byte[DIGEST.getDigestSize()];
DIGEST.doFinal(out, 0);
return out;
}
/*
* Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the
* pseudorandom function (PRF) and with dkLen == output length of
* HMAC() == output length of H().
*/
private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations)
throws InvalidKeyException {
byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE));
byte[] out = u.clone();
for (int i = 1; i < iterations; i++) {
u = hmac(key, u);
for (int j = 0; j < u.length; j++) {
out[j] ^= u[j];
}
}
return out;
}
}

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

@ -3,8 +3,10 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.SystemClock;
import android.util.Pair;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
import net.java.otr4j.crypto.OtrCryptoException;
@ -13,16 +15,20 @@ import org.json.JSONObject;
import java.security.PublicKey;
import java.security.interfaces.DSAPublicKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@ -41,6 +47,8 @@ public class Account extends AbstractEntity {
public static final String DISPLAY_NAME = "display_name";
public static final String HOSTNAME = "hostname";
public static final String PORT = "port";
public static final String STATUS = "status";
public static final String STATUS_MESSAGE = "status_message";
public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
@ -48,9 +56,15 @@ public class Account extends AbstractEntity {
public static final int OPTION_DISABLED = 1;
public static final int OPTION_REGISTER = 2;
public static final int OPTION_USECOMPRESSION = 3;
public static final int OPTION_MAGIC_CREATE = 4;
public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
public boolean httpUploadAvailable(long filesize) {
return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
}
public boolean httpUploadAvailable() {
return xmppConnection != null && xmppConnection.getFeatures().httpUpload();
return httpUploadAvailable(0);
}
public void setDisplayName(String displayName) {
@ -69,6 +83,29 @@ public class Account extends AbstractEntity {
}
}
public Contact getSelfContact() {
return getRoster().getContact(jid);
}
public boolean hasPendingPgpIntent(Conversation conversation) {
return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
}
public boolean isPgpDecryptionServiceConnected() {
return pgpDecryptionService != null && pgpDecryptionService.isConnected();
}
public boolean setShowErrorNotification(boolean newValue) {
boolean oldValue = showErrorNotification();
setKey("show_error",Boolean.toString(newValue));
return newValue != oldValue;
}
public boolean showErrorNotification() {
String key = getKey("show_error");
return key == null || Boolean.parseBoolean(key);
}
public enum State {
DISABLED,
OFFLINE,
@ -83,7 +120,15 @@ public class Account extends AbstractEntity {
REGISTRATION_NOT_SUPPORTED(true),
SECURITY_ERROR(true),
INCOMPATIBLE_SERVER(true),
TOR_NOT_AVAILABLE(true);
TOR_NOT_AVAILABLE(true),
BIND_FAILURE(true),
HOST_UNKNOWN(true),
REGISTRATION_PLEASE_WAIT(true),
STREAM_ERROR(true),
POLICY_VIOLATION(true),
REGISTRATION_PASSWORD_TOO_WEAK(true),
PAYMENT_REQUIRED(true),
MISSING_INTERNET_PERMISSION(true);
private final boolean isError;
@ -91,11 +136,11 @@ public class Account extends AbstractEntity {
return this.isError;
}
private State(final boolean isError) {
State(final boolean isError) {
this.isError = isError;
}
private State() {
State() {
this(false);
}
@ -129,6 +174,22 @@ public class Account extends AbstractEntity {
return R.string.account_status_incompatible_server;
case TOR_NOT_AVAILABLE:
return R.string.account_status_tor_unavailable;
case BIND_FAILURE:
return R.string.account_status_bind_failure;
case HOST_UNKNOWN:
return R.string.account_status_host_unknown;
case POLICY_VIOLATION:
return R.string.account_status_policy_violation;
case REGISTRATION_PLEASE_WAIT:
return R.string.registration_please_wait;
case REGISTRATION_PASSWORD_TOO_WEAK:
return R.string.registration_password_too_weak;
case STREAM_ERROR:
return R.string.account_status_stream_error;
case PAYMENT_REQUIRED:
return R.string.payment_required;
case MISSING_INTERNET_PERMISSION:
return R.string.missing_internet_permission;
default:
return R.string.account_status_unknown;
}
@ -146,7 +207,7 @@ public class Account extends AbstractEntity {
protected int options = 0;
protected String rosterVersion;
protected State status = State.OFFLINE;
protected JSONObject keys = new JSONObject();
protected final JSONObject keys;
protected String avatar;
protected String displayName = null;
protected String hostname = null;
@ -161,15 +222,18 @@ public class Account extends AbstractEntity {
private final Roster roster = new Roster(this);
private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
private Presence.Status presenceStatus = Presence.Status.ONLINE;
private String presenceStatusMessage = null;
public Account(final Jid jid, final String password) {
this(java.util.UUID.randomUUID().toString(), jid,
password, 0, null, "", null, null, null, 5222);
password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
}
private Account(final String uuid, final Jid jid,
final String password, final int options, final String rosterVersion, final String keys,
final String avatar, String displayName, String hostname, int port) {
final String password, final int options, final String rosterVersion, final String keys,
final String avatar, String displayName, String hostname, int port,
final Presence.Status status, String statusMessage) {
this.uuid = uuid;
this.jid = jid;
if (jid.isBareJid()) {
@ -178,15 +242,19 @@ public class Account extends AbstractEntity {
this.password = password;
this.options = options;
this.rosterVersion = rosterVersion;
JSONObject tmp;
try {
this.keys = new JSONObject(keys);
} catch (final JSONException ignored) {
this.keys = new JSONObject();
tmp = new JSONObject(keys);
} catch(JSONException e) {
tmp = new JSONObject();
}
this.keys = tmp;
this.avatar = avatar;
this.displayName = displayName;
this.hostname = hostname;
this.port = port;
this.presenceStatus = status;
this.presenceStatusMessage = statusMessage;
}
public static Account fromCursor(final Cursor cursor) {
@ -205,7 +273,9 @@ public class Account extends AbstractEntity {
cursor.getString(cursor.getColumnIndex(AVATAR)),
cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
cursor.getString(cursor.getColumnIndex(HOSTNAME)),
cursor.getInt(cursor.getColumnIndex(PORT)));
cursor.getInt(cursor.getColumnIndex(PORT)),
Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
}
public boolean isOptionSet(final int option) {
@ -224,8 +294,10 @@ public class Account extends AbstractEntity {
return jid.getLocalpart();
}
public void setJid(final Jid jid) {
this.jid = jid;
public boolean setJid(final Jid next) {
final Jid prev = this.jid != null ? this.jid.toBareJid() : null;
this.jid = next;
return prev == null || (next != null && !prev.equals(next.toBareJid()));
}
public Jid getServer() {
@ -249,7 +321,8 @@ public class Account extends AbstractEntity {
}
public boolean isOnion() {
return getServer().toString().toLowerCase().endsWith(".onion");
final Jid server = getServer();
return server != null && server.toString().toLowerCase().endsWith(".onion");
}
public void setPort(int port) {
@ -268,6 +341,10 @@ public class Account extends AbstractEntity {
}
}
public State getTrueStatus() {
return this.status;
}
public void setStatus(final State status) {
this.status = status;
}
@ -277,7 +354,25 @@ public class Account extends AbstractEntity {
}
public boolean hasErrorStatus() {
return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 3;
return getXmppConnection() != null
&& (getStatus().isError() || getStatus() == State.CONNECTING)
&& getXmppConnection().getAttempt() >= 3;
}
public void setPresenceStatus(Presence.Status status) {
this.presenceStatus = status;
}
public Presence.Status getPresenceStatus() {
return this.presenceStatus;
}
public void setPresenceStatusMessage(String message) {
this.presenceStatusMessage = message;
}
public String getPresenceStatusMessage() {
return this.presenceStatusMessage;
}
public String getResource() {
@ -306,15 +401,28 @@ public class Account extends AbstractEntity {
}
public String getKey(final String name) {
return this.keys.optString(name, null);
synchronized (this.keys) {
return this.keys.optString(name, null);
}
}
public int getKeyAsInt(final String name, int defaultValue) {
String key = getKey(name);
try {
return key == null ? defaultValue : Integer.parseInt(key);
} catch (NumberFormatException e) {
return defaultValue;
}
}
public boolean setKey(final String keyName, final String keyValue) {
try {
this.keys.put(keyName, keyValue);
return true;
} catch (final JSONException e) {
return false;
synchronized (this.keys) {
try {
this.keys.put(keyName, keyValue);
return true;
} catch (final JSONException e) {
return false;
}
}
}
@ -334,12 +442,16 @@ public class Account extends AbstractEntity {
values.put(SERVER, jid.getDomainpart());
values.put(PASSWORD, password);
values.put(OPTIONS, options);
values.put(KEYS, this.keys.toString());
synchronized (this.keys) {
values.put(KEYS, this.keys.toString());
}
values.put(ROSTERVERSION, rosterVersion);
values.put(AVATAR, avatar);
values.put(DISPLAY_NAME, displayName);
values.put(HOSTNAME, hostname);
values.put(PORT, port);
values.put(STATUS, presenceStatus.toShowString());
values.put(STATUS_MESSAGE, presenceStatusMessage);
return values;
}
@ -350,10 +462,10 @@ public class Account extends AbstractEntity {
public void initAccountServices(final XmppConnectionService context) {
this.mOtrService = new OtrService(context, this);
this.axolotlService = new AxolotlService(this, context);
this.pgpDecryptionService = new PgpDecryptionService(context);
if (xmppConnection != null) {
xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
}
this.pgpDecryptionService = new PgpDecryptionService(context);
}
public OtrService getOtrService() {
@ -361,7 +473,7 @@ public class Account extends AbstractEntity {
}
public PgpDecryptionService getPgpDecryptionService() {
return pgpDecryptionService;
return this.pgpDecryptionService;
}
public XmppConnection getXmppConnection() {
@ -382,7 +494,7 @@ public class Account extends AbstractEntity {
if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
return null;
}
this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey).toLowerCase(Locale.US);
return this.otrFingerprint;
} catch (final OtrCryptoException ignored) {
return null;
@ -405,58 +517,46 @@ public class Account extends AbstractEntity {
}
public int countPresences() {
return this.getRoster().getContact(this.getJid().toBareJid()).getPresences().size();
return this.getSelfContact().getPresences().size();
}
public String getPgpSignature() {
try {
if (keys.has(KEY_PGP_SIGNATURE) && !"null".equals(keys.getString(KEY_PGP_SIGNATURE))) {
return keys.getString(KEY_PGP_SIGNATURE);
} else {
return null;
}
} catch (final JSONException e) {
return null;
}
return getKey(KEY_PGP_SIGNATURE);
}
public boolean setPgpSignature(String signature) {
try {
keys.put(KEY_PGP_SIGNATURE, signature);
} catch (JSONException e) {
return false;
}
return true;
return setKey(KEY_PGP_SIGNATURE, signature);
}
public boolean unsetPgpSignature() {
try {
keys.put(KEY_PGP_SIGNATURE, JSONObject.NULL);
} catch (JSONException e) {
return false;
synchronized (this.keys) {
return keys.remove(KEY_PGP_SIGNATURE) != null;
}
return true;
}
public long getPgpId() {
if (keys.has(KEY_PGP_ID)) {
try {
return keys.getLong(KEY_PGP_ID);
} catch (JSONException e) {
return -1;
synchronized (this.keys) {
if (keys.has(KEY_PGP_ID)) {
try {
return keys.getLong(KEY_PGP_ID);
} catch (JSONException e) {
return 0;
}
} else {
return 0;
}
} else {
return -1;
}
}
public boolean setPgpSignId(long pgpID) {
try {
keys.put(KEY_PGP_ID, pgpID);
} catch (JSONException e) {
return false;
synchronized (this.keys) {
try {
keys.put(KEY_PGP_ID, pgpID);
} catch (JSONException e) {
return false;
}
return true;
}
return true;
}
public Roster getRoster() {
@ -494,9 +594,8 @@ public class Account extends AbstractEntity {
return this.avatar;
}
public void activateGracePeriod() {
this.mEndGracePeriod = SystemClock.elapsedRealtime()
+ (Config.CARBON_GRACE_PERIOD * 1000);
public void activateGracePeriod(long duration) {
this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
}
public void deactivateGracePeriod() {
@ -508,14 +607,43 @@ public class Account extends AbstractEntity {
}
public String getShareableUri() {
final String fingerprint = this.getOtrFingerprint();
if (fingerprint != null) {
return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint;
List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
String uri = "xmpp:"+this.getJid().toBareJid().toString();
if (fingerprints.size() > 0) {
return XmppUri.getFingerprintUri(uri,fingerprints,';');
} else {
return "xmpp:" + this.getJid().toBareJid().toString();
return uri;
}
}
public String getShareableLink() {
List<XmppUri.Fingerprint> fingerprints = this.getFingerprints();
String uri = "https://conversations.im/i/"+this.getJid().toBareJid().toString();
if (fingerprints.size() > 0) {
return XmppUri.getFingerprintUri(uri,fingerprints,'&');
} else {
return uri;
}
}
private List<XmppUri.Fingerprint> getFingerprints() {
ArrayList<XmppUri.Fingerprint> fingerprints = new ArrayList<>();
final String otr = this.getOtrFingerprint();
if (otr != null) {
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OTR,otr));
}
if (axolotlService == null) {
return fingerprints;
}
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,axolotlService.getOwnFingerprint().substring(2),axolotlService.getOwnDeviceId()));
for(XmppAxolotlSession session : axolotlService.findOwnSessions()) {
if (session.getTrust().isVerified() && session.getTrust().isActive()) {
fingerprints.add(new XmppUri.Fingerprint(XmppUri.FingerprintType.OMEMO,session.getFingerprint().substring(2).replaceAll("\\s",""),session.getRemoteAddress().getDeviceId()));
}
}
return fingerprints;
}
public boolean isBlocked(final ListItem contact) {
final Jid jid = contact.getJid();
return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));

View file

@ -1,9 +1,12 @@
package eu.siacs.conversations.entities;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
@ -47,13 +50,25 @@ public class Bookmark extends Element implements ListItem {
@Override
public String getDisplayName() {
if (this.mJoinedConversation != null
&& (this.mJoinedConversation.getMucOptions().getSubject() != null)) {
return this.mJoinedConversation.getMucOptions().getSubject();
} else if (getBookmarkName() != null) {
return getBookmarkName();
if (this.mJoinedConversation != null) {
return this.mJoinedConversation.getName();
} else if (getBookmarkName() != null
&& !getBookmarkName().trim().isEmpty()) {
return getBookmarkName().trim();
} else {
return this.getJid().getLocalpart();
Jid jid = this.getJid();
String name = jid != null ? jid.getLocalpart() : getAttribute("jid");
return name != null ? name : "";
}
}
@Override
public String getDisplayJid() {
Jid jid = getJid();
if (jid != null) {
return jid.toString();
} else {
return getAttribute("jid"); //fallback if jid wasn't parsable
}
}
@ -63,8 +78,8 @@ public class Bookmark extends Element implements ListItem {
}
@Override
public List<Tag> getTags() {
ArrayList<Tag> tags = new ArrayList<Tag>();
public List<Tag> getTags(Context context) {
ArrayList<Tag> tags = new ArrayList<>();
for (Element element : getChildren()) {
if (element.getName().equals("group") && element.getContent() != null) {
String group = element.getContent();
@ -101,7 +116,8 @@ public class Bookmark extends Element implements ListItem {
}
}
public boolean match(String needle) {
@Override
public boolean match(Context context, String needle) {
if (needle == null) {
return true;
}
@ -109,12 +125,12 @@ public class Bookmark extends Element implements ListItem {
final Jid jid = getJid();
return (jid != null && jid.toString().contains(needle)) ||
getDisplayName().toLowerCase(Locale.US).contains(needle) ||
matchInTag(needle);
matchInTag(context, needle);
}
private boolean matchInTag(String needle) {
private boolean matchInTag(Context context, String needle) {
needle = needle.toLowerCase(Locale.US);
for (Tag tag : getTags()) {
for (Tag tag : getTags(context)) {
if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
return true;
}

View file

@ -1,7 +1,10 @@
package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.ContactsContract;
import org.json.JSONArray;
import org.json.JSONException;
@ -33,24 +36,29 @@ public class Contact implements ListItem, Blockable {
public static final String LAST_PRESENCE = "last_presence";
public static final String LAST_TIME = "last_time";
public static final String GROUPS = "groups";
public Lastseen lastseen = new Lastseen();
protected String accountUuid;
protected String systemName;
protected String serverName;
protected String presenceName;
protected String commonName;
protected Jid jid;
protected int subscription = 0;
protected String systemAccount;
protected String photoUri;
protected JSONObject keys = new JSONObject();
protected JSONArray groups = new JSONArray();
protected Presences presences = new Presences();
protected final Presences presences = new Presences();
protected Account account;
protected Avatar avatar;
private boolean mActive = false;
private long mLastseen = 0;
private String mLastPresence = null;
public Contact(final String account, final String systemName, final String serverName,
final Jid jid, final int subscription, final String photoUri,
final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) {
final String systemAccount, final String keys, final String avatar, final long lastseen,
final String presence, final String groups) {
this.accountUuid = account;
this.systemName = systemName;
this.serverName = serverName;
@ -73,7 +81,8 @@ public class Contact implements ListItem, Blockable {
} catch (JSONException e) {
this.groups = new JSONArray();
}
this.lastseen = lastseen;
this.mLastseen = lastseen;
this.mLastPresence = presence;
}
public Contact(final Jid jid) {
@ -81,9 +90,6 @@ public class Contact implements ListItem, Blockable {
}
public static Contact fromCursor(final Cursor cursor) {
final Lastseen lastseen = new Lastseen(
cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
cursor.getLong(cursor.getColumnIndex(LAST_TIME)));
final Jid jid;
try {
jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true);
@ -100,26 +106,36 @@ public class Contact implements ListItem, Blockable {
cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
cursor.getString(cursor.getColumnIndex(KEYS)),
cursor.getString(cursor.getColumnIndex(AVATAR)),
lastseen,
cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
cursor.getString(cursor.getColumnIndex(GROUPS)));
}
public String getDisplayName() {
if (this.presenceName != null && Config.X509_VERIFICATION) {
return this.presenceName;
if (this.commonName != null && Config.X509_VERIFICATION) {
return this.commonName;
} else if (this.systemName != null) {
return this.systemName;
} else if (this.serverName != null) {
return this.serverName;
} else if (this.presenceName != null) {
} else if (this.presenceName != null && mutualPresenceSubscription()) {
return this.presenceName;
} else if (jid.hasLocalpart()) {
return jid.getLocalpart();
return jid.getUnescapedLocalpart();
} else {
return jid.getDomainpart();
}
}
@Override
public String getDisplayJid() {
if (jid != null) {
return jid.toString();
} else {
return null;
}
}
public String getProfilePhoto() {
return this.photoUri;
}
@ -129,25 +145,14 @@ public class Contact implements ListItem, Blockable {
}
@Override
public List<Tag> getTags() {
public List<Tag> getTags(Context context) {
final ArrayList<Tag> tags = new ArrayList<>();
for (final String group : getGroups()) {
tags.add(new Tag(group, UIHelper.getColorForName(group)));
}
switch (getMostAvailableStatus()) {
case Presences.CHAT:
case Presences.ONLINE:
tags.add(new Tag("online", 0xff259b24));
break;
case Presences.AWAY:
tags.add(new Tag("away", 0xffff9800));
break;
case Presences.XA:
tags.add(new Tag("not available", 0xfff44336));
break;
case Presences.DND:
tags.add(new Tag("dnd", 0xfff44336));
break;
Presence.Status status = getShownStatus();
if (status != Presence.Status.OFFLINE) {
tags.add(UIHelper.getTagForStatus(context, status));
}
if (isBlocked()) {
tags.add(new Tag("blocked", 0xff2e2f3b));
@ -155,7 +160,7 @@ public class Contact implements ListItem, Blockable {
return tags;
}
public boolean match(String needle) {
public boolean match(Context context, String needle) {
if (needle == null || needle.isEmpty()) {
return true;
}
@ -163,7 +168,7 @@ public class Contact implements ListItem, Blockable {
String[] parts = needle.split("\\s+");
if (parts.length > 1) {
for(int i = 0; i < parts.length; ++i) {
if (!match(parts[i])) {
if (!match(context, parts[i])) {
return false;
}
}
@ -171,13 +176,13 @@ public class Contact implements ListItem, Blockable {
} else {
return jid.toString().contains(needle) ||
getDisplayName().toLowerCase(Locale.US).contains(needle) ||
matchInTag(needle);
matchInTag(context, needle);
}
}
private boolean matchInTag(String needle) {
private boolean matchInTag(Context context, String needle) {
needle = needle.toLowerCase(Locale.US);
for (Tag tag : getTags()) {
for (Tag tag : getTags(context)) {
if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
return true;
}
@ -191,23 +196,19 @@ public class Contact implements ListItem, Blockable {
values.put(ACCOUNT, accountUuid);
values.put(SYSTEMNAME, systemName);
values.put(SERVERNAME, serverName);
values.put(JID, jid.toString());
values.put(JID, jid.toPreppedString());
values.put(OPTIONS, subscription);
values.put(SYSTEMACCOUNT, systemAccount);
values.put(PHOTOURI, photoUri);
values.put(KEYS, keys.toString());
values.put(AVATAR, avatar == null ? null : avatar.getFilename());
values.put(LAST_PRESENCE, lastseen.presence);
values.put(LAST_TIME, lastseen.time);
values.put(LAST_PRESENCE, mLastPresence);
values.put(LAST_TIME, mLastseen);
values.put(GROUPS, groups.toString());
return values;
}
}
public int getSubscription() {
return this.subscription;
}
public Account getAccount() {
return this.account;
}
@ -221,12 +222,8 @@ public class Contact implements ListItem, Blockable {
return this.presences;
}
public void setPresences(Presences pres) {
this.presences = pres;
}
public void updatePresence(final String resource, final int status) {
this.presences.updatePresence(resource, status);
public void updatePresence(final String resource, final Presence presence) {
this.presences.updatePresence(resource, presence);
}
public void removePresence(final String resource) {
@ -238,8 +235,8 @@ public class Contact implements ListItem, Blockable {
this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
}
public int getMostAvailableStatus() {
return this.presences.getMostAvailableStatus();
public Presence.Status getShownStatus() {
return this.presences.getShownStatus();
}
public boolean setPhotoUri(String uri) {
@ -266,8 +263,18 @@ public class Contact implements ListItem, Blockable {
this.presenceName = presenceName;
}
public String getSystemAccount() {
return systemAccount;
public Uri getSystemAccount() {
if (systemAccount == null) {
return null;
} else {
String[] parts = systemAccount.split("#");
if (parts.length != 2) {
return null;
} else {
long id = Long.parseLong(parts[0]);
return ContactsContract.Contacts.getLookupUri(id, parts[1]);
}
}
}
public void setSystemAccount(String account) {
@ -294,7 +301,7 @@ public class Contact implements ListItem, Blockable {
for (int i = 0; i < prints.length(); ++i) {
final String print = prints.isNull(i) ? null : prints.getString(i);
if (print != null && !print.isEmpty()) {
fingerprints.add(prints.getString(i));
fingerprints.add(prints.getString(i).toLowerCase(Locale.US));
}
}
}
@ -380,11 +387,13 @@ public class Contact implements ListItem, Blockable {
this.resetOption(Options.TO);
this.setOption(Options.FROM);
this.resetOption(Options.PREEMPTIVE_GRANT);
this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
break;
case "both":
this.setOption(Options.TO);
this.setOption(Options.FROM);
this.resetOption(Options.PREEMPTIVE_GRANT);
this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
break;
case "none":
this.resetOption(Options.FROM);
@ -474,19 +483,10 @@ public class Contact implements ListItem, Blockable {
}
}
public boolean trusted() {
public boolean mutualPresenceSubscription() {
return getOption(Options.FROM) && getOption(Options.TO);
}
public String getShareableUri() {
if (getOtrFingerprints().size() >= 1) {
String otr = getOtrFingerprints().get(0);
return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr;
} else {
return "xmpp:" + getJid().toBareJid().toString();
}
}
@Override
public boolean isBlocked() {
return getAccount().isBlocked(this);
@ -510,18 +510,36 @@ public class Contact implements ListItem, Blockable {
return account.getJid().toBareJid().equals(getJid().toBareJid());
}
public static class Lastseen {
public long time;
public String presence;
public void setCommonName(String cn) {
this.commonName = cn;
}
public Lastseen() {
this(null, 0);
}
public void flagActive() {
this.mActive = true;
}
public Lastseen(final String presence, final long time) {
this.presence = presence;
this.time = time;
}
public void flagInactive() {
this.mActive = false;
}
public boolean isActive() {
return this.mActive;
}
public void setLastseen(long timestamp) {
this.mLastseen = Math.max(timestamp, mLastseen);
}
public long getLastseen() {
return this.mLastseen;
}
public void setLastResource(String resource) {
this.mLastPresence = resource;
}
public String getLastResource() {
return this.mLastPresence;
}
public final class Options {

View file

@ -9,23 +9,30 @@ import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.security.interfaces.DSAPublicKey;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class Conversation extends AbstractEntity implements Blockable {
public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation> {
public static final String TABLENAME = "conversations";
public static final int STATUS_AVAILABLE = 0;
@ -48,6 +55,8 @@ public class Conversation extends AbstractEntity implements Blockable {
public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
public static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
private String name;
private String contactUuid;
@ -81,6 +90,9 @@ public class Conversation extends AbstractEntity implements Blockable {
private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
private String mLastReceivedOtrMessageId = null;
private String mFirstMamReference = null;
private Message correctingMessage;
public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
public boolean hasMessagesLeftOnServer() {
return messagesLeftOnServer;
@ -90,6 +102,21 @@ public class Conversation extends AbstractEntity implements Blockable {
this.messagesLeftOnServer = value;
}
public Message getFirstUnreadMessage() {
Message first = null;
synchronized (this.messages) {
for (int i = messages.size() - 1; i >= 0; --i) {
if (messages.get(i).isRead()) {
return first;
} else {
first = messages.get(i);
}
}
}
return first;
}
public Message findUnsentMessageWithUuid(String uuid) {
synchronized(this.messages) {
for (final Message message : this.messages) {
@ -136,9 +163,9 @@ public class Conversation extends AbstractEntity implements Blockable {
public Message findMessageWithFileAndUuid(final String uuid) {
synchronized (this.messages) {
for (final Message message : this.messages) {
if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
if (message.getUuid().equals(uuid)
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& message.getUuid().equals(uuid)) {
&& (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.treatAsDownloadable() != Message.Decision.NEVER)) {
return message;
}
}
@ -185,7 +212,13 @@ public class Conversation extends AbstractEntity implements Blockable {
final int size = messages.size();
final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
if (size > maxsize) {
this.messages.subList(0, size - maxsize).clear();
List<Message> discards = this.messages.subList(0, size - maxsize);
final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
if (pgpDecryptionService != null) {
pgpDecryptionService.discard(discards);
}
discards.clear();
untieMessages();
}
}
}
@ -225,6 +258,24 @@ public class Conversation extends AbstractEntity implements Blockable {
return null;
}
public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
synchronized (this.messages) {
for(int i = this.messages.size() - 1; i >= 0; --i) {
Message message = messages.get(i);
if (counterpart.equals(message.getCounterpart())
&& ((message.getStatus() == Message.STATUS_RECEIVED) == received)
&& (carbon == message.isCarbon() || received) ) {
if (id.equals(message.getRemoteMsgId())) {
return message;
} else {
return null;
}
}
}
}
return null;
}
public Message findSentMessageWithUuid(String id) {
synchronized (this.messages) {
for (Message message : this.messages) {
@ -277,6 +328,59 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
public void setFirstMamReference(String reference) {
this.mFirstMamReference = reference;
}
public String getFirstMamReference() {
return this.mFirstMamReference;
}
public void setLastClearHistory(long time) {
setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY,String.valueOf(time));
}
public long getLastClearHistory() {
return getLongAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, 0);
}
public List<Jid> getAcceptedCryptoTargets() {
if (mode == MODE_SINGLE) {
return Arrays.asList(getJid().toBareJid());
} else {
return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
}
}
public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
}
public void setCorrectingMessage(Message correctingMessage) {
this.correctingMessage = correctingMessage;
}
public Message getCorrectingMessage() {
return this.correctingMessage;
}
public boolean withSelf() {
return getContact().isSelf();
}
@Override
public int compareTo(Conversation another) {
final Message left = getLatestMessage();
final Message right = another.getLatestMessage();
if (left.getTimeSent() > right.getTimeSent()) {
return -1;
} else if (left.getTimeSent() < right.getTimeSent()) {
return 1;
} else {
return 0;
}
}
public interface OnMessageFound {
void onMessageFound(final Message message);
}
@ -354,14 +458,16 @@ public class Conversation extends AbstractEntity implements Blockable {
if (getMode() == MODE_MULTI) {
if (getMucOptions().getSubject() != null) {
return getMucOptions().getSubject();
} else if (bookmark != null && bookmark.getBookmarkName() != null) {
return bookmark.getBookmarkName();
} else if (bookmark != null
&& bookmark.getBookmarkName() != null
&& !bookmark.getBookmarkName().trim().isEmpty()) {
return bookmark.getBookmarkName().trim();
} else {
String generatedName = getMucOptions().createNameFromParticipants();
if (generatedName != null) {
return generatedName;
} else {
return getJid().getLocalpart();
return getJid().getUnescapedLocalpart();
}
}
} else {
@ -404,7 +510,7 @@ public class Conversation extends AbstractEntity implements Blockable {
values.put(NAME, name);
values.put(CONTACT, contactUuid);
values.put(ACCOUNT, accountUuid);
values.put(CONTACTJID, contactJid.toString());
values.put(CONTACTJID, contactJid.toPreppedString());
values.put(CREATED, created);
values.put(STATUS, status);
values.put(MODE, mode);
@ -480,15 +586,18 @@ public class Conversation extends AbstractEntity implements Blockable {
return mSmp;
}
public void startOtrIfNeeded() {
if (this.otrSession != null
&& this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
public boolean startOtrIfNeeded() {
if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
try {
this.otrSession.startSession();
return true;
} catch (OtrException e) {
this.resetOtrSession();
return false;
}
}
} else {
return true;
}
}
public boolean endOtrIfNeeded() {
@ -522,7 +631,7 @@ public class Conversation extends AbstractEntity implements Blockable {
return null;
}
DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
return null;
}
@ -574,66 +683,41 @@ public class Conversation extends AbstractEntity implements Blockable {
return this.nextCounterpart;
}
private int getMostRecentlyUsedOutgoingEncryption() {
synchronized (this.messages) {
for(int i = this.messages.size() -1; i >= 0; --i) {
final Message m = this.messages.get(i);
if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
final int e = m.getEncryption();
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
return Message.ENCRYPTION_PGP;
} else {
return e;
}
}
}
}
return Message.ENCRYPTION_NONE;
}
private int getMostRecentlyUsedIncomingEncryption() {
synchronized (this.messages) {
for(int i = this.messages.size() -1; i >= 0; --i) {
final Message m = this.messages.get(i);
if (m.getStatus() == Message.STATUS_RECEIVED) {
final int e = m.getEncryption();
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
return Message.ENCRYPTION_PGP;
} else {
return e;
}
}
}
}
return Message.ENCRYPTION_NONE;
}
public int getNextEncryption() {
final AxolotlService axolotlService = getAccount().getAxolotlService();
int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
if (next == -1) {
if (Config.X509_VERIFICATION && mode == MODE_SINGLE) {
if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
return Message.ENCRYPTION_AXOLOTL;
} else {
return Message.ENCRYPTION_NONE;
}
}
int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
if (outgoing == Message.ENCRYPTION_NONE) {
next = this.getMostRecentlyUsedIncomingEncryption();
} else {
next = outgoing;
}
return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
}
private int fixAvailableEncryption(int selectedEncryption) {
switch(selectedEncryption) {
case Message.ENCRYPTION_NONE:
return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
case Message.ENCRYPTION_AXOLOTL:
return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
case Message.ENCRYPTION_OTR:
return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
case Message.ENCRYPTION_PGP:
case Message.ENCRYPTION_DECRYPTED:
case Message.ENCRYPTION_DECRYPTION_FAILED:
return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
default:
return getDefaultEncryption();
}
if (Config.FORCE_E2E_ENCRYPTION && mode == MODE_SINGLE && next <= 0) {
if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) {
return Message.ENCRYPTION_AXOLOTL;
} else {
return Message.ENCRYPTION_OTR;
}
}
private int getDefaultEncryption() {
AxolotlService axolotlService = account.getAxolotlService();
if (Config.supportUnencrypted()) {
return Message.ENCRYPTION_NONE;
} else if (Config.supportOmemo()
&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
return Message.ENCRYPTION_AXOLOTL;
} else if (Config.supportOtr() && mode == MODE_SINGLE) {
return Message.ENCRYPTION_OTR;
} else if (Config.supportOpenPgp()) {
return Message.ENCRYPTION_PGP;
} else {
return Message.ENCRYPTION_NONE;
}
return next;
}
public void setNextEncryption(int encryption) {
@ -682,7 +766,7 @@ public class Conversation extends AbstractEntity implements Blockable {
public boolean hasDuplicateMessage(Message message) {
synchronized (this.messages) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
if (this.messages.get(i).equals(message)) {
if (this.messages.get(i).similar(message)) {
return true;
}
}
@ -711,15 +795,18 @@ public class Conversation extends AbstractEntity implements Blockable {
}
public long getLastMessageTransmitted() {
final long last_clear = getLastClearHistory();
long last_received = 0;
synchronized (this.messages) {
for(int i = this.messages.size() - 1; i >= 0; --i) {
Message message = this.messages.get(i);
if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
return message.getTimeSent();
last_received = message.getTimeSent();
break;
}
}
}
return 0;
return Math.max(last_clear,last_received);
}
public void setMutedTill(long value) {
@ -731,26 +818,65 @@ public class Conversation extends AbstractEntity implements Blockable {
}
public boolean alwaysNotify() {
return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY,Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
}
public boolean setAttribute(String key, String value) {
try {
this.attributes.put(key, value);
return true;
} catch (JSONException e) {
return false;
synchronized (this.attributes) {
try {
this.attributes.put(key, value);
return true;
} catch (JSONException e) {
return false;
}
}
}
public boolean setAttribute(String key, List<Jid> jids) {
JSONArray array = new JSONArray();
for(Jid jid : jids) {
array.put(jid.toBareJid().toString());
}
synchronized (this.attributes) {
try {
this.attributes.put(key, array);
return true;
} catch (JSONException e) {
e.printStackTrace();
return false;
}
}
}
public String getAttribute(String key) {
try {
return this.attributes.getString(key);
} catch (JSONException e) {
return null;
synchronized (this.attributes) {
try {
return this.attributes.getString(key);
} catch (JSONException e) {
return null;
}
}
}
public List<Jid> getJidListAttribute(String key) {
ArrayList<Jid> list = new ArrayList<>();
synchronized (this.attributes) {
try {
JSONArray array = this.attributes.getJSONArray(key);
for (int i = 0; i < array.length(); ++i) {
try {
list.add(Jid.fromString(array.getString(i)));
} catch (InvalidJidException e) {
//ignored
}
}
} catch (JSONException e) {
//ignored
}
}
return list;
}
public int getIntAttribute(String key, int defaultValue) {
String value = this.getAttribute(key);
if (value == null) {
@ -793,11 +919,29 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
public void prepend(Message message) {
message.setConversation(this);
synchronized (this.messages) {
this.messages.add(0,message);
}
}
public void addAll(int index, List<Message> messages) {
synchronized (this.messages) {
this.messages.addAll(index, messages);
}
account.getPgpDecryptionService().addAll(messages);
account.getPgpDecryptionService().decrypt(messages);
}
public void expireOldMessages(long timestamp) {
synchronized (this.messages) {
for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
if (iterator.next().getTimeSent() < timestamp) {
iterator.remove();
}
}
untieMessages();
}
}
public void sort() {
@ -814,9 +958,13 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
});
for(Message message : this.messages) {
message.untie();
}
untieMessages();
}
}
private void untieMessages() {
for(Message message : this.messages) {
message.untie();
}
}

View file

@ -52,20 +52,16 @@ public class DownloadableFile extends File {
public void setKeyAndIv(byte[] keyIvCombo) {
if (keyIvCombo.length == 48) {
byte[] secretKey = new byte[32];
byte[] iv = new byte[16];
System.arraycopy(keyIvCombo, 0, iv, 0, 16);
System.arraycopy(keyIvCombo, 16, secretKey, 0, 32);
this.aeskey = secretKey;
this.iv = iv;
this.aeskey = new byte[32];
this.iv = new byte[16];
System.arraycopy(keyIvCombo, 0, this.iv, 0, 16);
System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32);
} else if (keyIvCombo.length >= 32) {
byte[] secretKey = new byte[32];
System.arraycopy(keyIvCombo, 0, secretKey, 0, 32);
this.aeskey = secretKey;
this.aeskey = new byte[32];
System.arraycopy(keyIvCombo, 0, aeskey, 0, 32);
} else if (keyIvCombo.length >= 16) {
byte[] secretKey = new byte[16];
System.arraycopy(keyIvCombo, 0, secretKey, 0, 16);
this.aeskey = secretKey;
this.aeskey = new byte[16];
System.arraycopy(keyIvCombo, 0, this.aeskey, 0, 16);
}
}

View file

@ -1,17 +1,21 @@
package eu.siacs.conversations.entities;
import android.content.Context;
import java.util.List;
import eu.siacs.conversations.xmpp.jid.Jid;
public interface ListItem extends Comparable<ListItem> {
public String getDisplayName();
String getDisplayName();
public Jid getJid();
String getDisplayJid();
public List<Tag> getTags();
Jid getJid();
public final class Tag {
List<Tag> getTags(Context context);
final class Tag {
private final String name;
private final int color;
@ -29,5 +33,5 @@ public interface ListItem extends Comparable<ListItem> {
}
}
public boolean match(final String needle);
boolean match(Context context, final String needle);
}

View file

@ -2,13 +2,15 @@ package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
import android.text.SpannableStringBuilder;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
@ -19,8 +21,6 @@ public class Message extends AbstractEntity {
public static final String TABLENAME = "messages";
public static final String MERGE_SEPARATOR = " \u200B\n\n";
public static final int STATUS_RECEIVED = 0;
public static final int STATUS_UNSEND = 1;
public static final int STATUS_SEND = 2;
@ -52,11 +52,14 @@ public class Message extends AbstractEntity {
public static final String STATUS = "status";
public static final String TYPE = "type";
public static final String CARBON = "carbon";
public static final String OOB = "oob";
public static final String EDITED = "edited";
public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
public static final String FINGERPRINT = "axolotl_fingerprint";
public static final String READ = "read";
public static final String ERROR_MESSAGE = "errorMsg";
public static final String ME_COMMAND = "/me ";
@ -71,6 +74,8 @@ public class Message extends AbstractEntity {
protected int status;
protected int type;
protected boolean carbon = false;
protected boolean oob = false;
protected String edited = null;
protected String relativeFilePath;
protected boolean read = true;
protected String remoteMsgId = null;
@ -80,6 +85,7 @@ public class Message extends AbstractEntity {
private Message mNextMessage = null;
private Message mPreviousMessage = null;
private String axolotlFingerprint = null;
private String errorMessage = null;
private Message() {
@ -104,7 +110,10 @@ public class Message extends AbstractEntity {
null,
null,
null,
true);
true,
null,
false,
null);
this.conversation = conversation;
}
@ -112,12 +121,13 @@ public class Message extends AbstractEntity {
final Jid trueCounterpart, final String body, final long timeSent,
final int encryption, final int status, final int type, final boolean carbon,
final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint, final boolean read) {
final String serverMsgId, final String fingerprint, final boolean read,
final String edited, final boolean oob, final String errorMessage) {
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
this.trueCounterpart = trueCounterpart;
this.body = body;
this.body = body == null ? "" : body;
this.timeSent = timeSent;
this.encryption = encryption;
this.status = status;
@ -128,6 +138,9 @@ public class Message extends AbstractEntity {
this.serverMsgId = serverMsgId;
this.axolotlFingerprint = fingerprint;
this.read = read;
this.edited = edited;
this.oob = oob;
this.errorMessage = errorMessage;
}
public static Message fromCursor(Cursor cursor) {
@ -162,12 +175,15 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(TYPE)),
cursor.getInt(cursor.getColumnIndex(CARBON))>0,
cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
cursor.getInt(cursor.getColumnIndex(READ)) > 0);
cursor.getInt(cursor.getColumnIndex(READ)) > 0,
cursor.getString(cursor.getColumnIndex(EDITED)),
cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)));
}
public static Message createStatusMessage(Conversation conversation, String body) {
@ -178,6 +194,14 @@ public class Message extends AbstractEntity {
return message;
}
public static Message createLoadMoreMessage(Conversation conversation) {
final Message message = new Message();
message.setType(Message.TYPE_STATUS);
message.setConversation(conversation);
message.setBody("LOAD_MORE");
return message;
}
@Override
public ContentValues getContentValues() {
ContentValues values = new ContentValues();
@ -186,12 +210,12 @@ public class Message extends AbstractEntity {
if (counterpart == null) {
values.putNull(COUNTERPART);
} else {
values.put(COUNTERPART, counterpart.toString());
values.put(COUNTERPART, counterpart.toPreppedString());
}
if (trueCounterpart == null) {
values.putNull(TRUE_COUNTERPART);
} else {
values.put(TRUE_COUNTERPART, trueCounterpart.toString());
values.put(TRUE_COUNTERPART, trueCounterpart.toPreppedString());
}
values.put(BODY, body);
values.put(TIME_SENT, timeSent);
@ -203,7 +227,10 @@ public class Message extends AbstractEntity {
values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId);
values.put(FINGERPRINT, axolotlFingerprint);
values.put(READ,read);
values.put(READ,read ? 1 : 0);
values.put(EDITED, edited);
values.put(OOB, oob ? 1 : 0);
values.put(ERROR_MESSAGE,errorMessage);
return values;
}
@ -245,9 +272,23 @@ public class Message extends AbstractEntity {
}
public void setBody(String body) {
if (body == null) {
throw new Error("You should not set the message body to null");
}
this.body = body;
}
public String getErrorMessage() {
return errorMessage;
}
public boolean setErrorMessage(String message) {
boolean changed = (message != null && !message.equals(errorMessage))
|| (message == null && errorMessage != null);
this.errorMessage = message;
return changed;
}
public long getTimeSent() {
return timeSent;
}
@ -332,10 +373,22 @@ public class Message extends AbstractEntity {
this.carbon = carbon;
}
public void setEdited(String edited) {
this.edited = edited;
}
public boolean edited() {
return this.edited != null;
}
public void setTrueCounterpart(Jid trueCounterpart) {
this.trueCounterpart = trueCounterpart;
}
public Jid getTrueCounterpart() {
return this.trueCounterpart;
}
public Transferable getTransferable() {
return this.transferable;
}
@ -344,8 +397,8 @@ public class Message extends AbstractEntity {
this.transferable = transferable;
}
public boolean equals(Message message) {
if (this.serverMsgId != null && message.getServerMsgId() != null) {
public boolean similar(Message message) {
if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
return this.serverMsgId.equals(message.getServerMsgId());
} else if (this.body == null || this.counterpart == null) {
return false;
@ -358,15 +411,18 @@ public class Message extends AbstractEntity {
body = this.body;
otherBody = message.body;
}
final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
if (message.getRemoteMsgId() != null) {
final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
if (hasUuid && this.edited != null && matchingCounterpart && this.edited.equals(message.getRemoteMsgId())) {
return true;
}
return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
&& this.counterpart.equals(message.getCounterpart())
&& (body.equals(otherBody)
||(message.getEncryption() == Message.ENCRYPTION_PGP
&& message.getRemoteMsgId().matches("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"))) ;
&& matchingCounterpart
&& (body.equals(otherBody) ||(message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
} else {
return this.remoteMsgId == null
&& this.counterpart.equals(message.getCounterpart())
&& matchingCounterpart
&& body.equals(otherBody)
&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
}
@ -401,19 +457,37 @@ public class Message extends AbstractEntity {
}
}
public boolean isLastCorrectableMessage() {
Message next = next();
while(next != null) {
if (next.isCorrectable()) {
return false;
}
next = next.next();
}
return isCorrectable();
}
private boolean isCorrectable() {
return getStatus() != STATUS_RECEIVED && !isCarbon();
}
public boolean mergeable(final Message message) {
return message != null &&
(message.getType() == Message.TYPE_TEXT &&
this.getTransferable() == null &&
message.getTransferable() == null &&
message.getEncryption() != Message.ENCRYPTION_PGP &&
message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
this.getType() == message.getType() &&
//this.getStatus() == message.getStatus() &&
isStatusMergeable(this.getStatus(), message.getStatus()) &&
this.getEncryption() == message.getEncryption() &&
this.getCounterpart() != null &&
this.getCounterpart().equals(message.getCounterpart()) &&
this.edited() == message.edited() &&
(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
!GeoHelper.isGeoUri(message.getBody()) &&
!GeoHelper.isGeoUri(this.body) &&
message.treatAsDownloadable() == Decision.NEVER &&
@ -422,7 +496,7 @@ public class Message extends AbstractEntity {
!this.getBody().startsWith(ME_COMMAND) &&
!this.bodyIsHeart() &&
!message.bodyIsHeart() &&
this.isTrusted() == message.isTrusted()
((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint()))
);
}
@ -437,19 +511,26 @@ public class Message extends AbstractEntity {
);
}
public String getMergedBody() {
StringBuilder body = new StringBuilder(this.body.trim());
public static class MergeSeparator {}
public SpannableStringBuilder getMergedBody() {
SpannableStringBuilder body = new SpannableStringBuilder(this.body.trim());
Message current = this;
while(current.mergeable(current.next())) {
while (current.mergeable(current.next())) {
current = current.next();
body.append(MERGE_SEPARATOR);
if (current == null) {
break;
}
body.append("\n\n");
body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
body.append(current.getBody().trim());
}
return body.toString();
return body;
}
public boolean hasMeCommand() {
return getMergedBody().startsWith(ME_COMMAND);
return this.body.trim().startsWith(ME_COMMAND);
}
public int getMergedStatus() {
@ -457,6 +538,9 @@ public class Message extends AbstractEntity {
Message current = this;
while(current.mergeable(current.next())) {
current = current.next();
if (current == null) {
break;
}
status = current.status;
}
return status;
@ -467,6 +551,9 @@ public class Message extends AbstractEntity {
Message current = this;
while(current.mergeable(current.next())) {
current = current.next();
if (current == null) {
break;
}
time = current.timeSent;
}
return time;
@ -479,7 +566,7 @@ public class Message extends AbstractEntity {
public boolean trusted() {
Contact contact = this.getContact();
return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
return (status > STATUS_RECEIVED || (contact != null && contact.mutualPresenceSubscription()));
}
public boolean fixCounterpart() {
@ -490,7 +577,7 @@ public class Message extends AbstractEntity {
try {
counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
conversation.getJid().getDomainpart(),
presences.asStringArray()[0]);
presences.toResourceArray()[0]);
return true;
} catch (InvalidJidException e) {
counterpart = null;
@ -502,6 +589,18 @@ public class Message extends AbstractEntity {
}
}
public void setUuid(String uuid) {
this.uuid = uuid;
}
public String getEditedId() {
return edited;
}
public void setOob(boolean isOob) {
this.oob = isOob;
}
public enum Decision {
MUST,
SHOULD,
@ -517,14 +616,14 @@ public class Message extends AbstractEntity {
if (path == null || path.isEmpty()) {
return null;
}
String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
int dotPosition = filename.lastIndexOf(".");
if (dotPosition != -1) {
String extension = filename.substring(dotPosition + 1);
// we want the real file extension, not the crypto one
if (Arrays.asList(Transferable.VALID_CRYPTO_EXTENSIONS).contains(extension)) {
if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
return extractRelevantExtension(filename.substring(0,dotPosition));
} else {
return extension;
@ -558,6 +657,8 @@ public class Message extends AbstractEntity {
URL url = new URL(body);
if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
return Decision.NEVER;
} else if (oob) {
return Decision.MUST;
}
String extension = extractRelevantExtension(url);
if (extension == null) {
@ -567,13 +668,9 @@ public class Message extends AbstractEntity {
boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
if (encrypted) {
if (MimeUtils.guessMimeTypeFromExtension(extension) != null) {
return Decision.MUST;
} else {
return Decision.NEVER;
}
} else if (Arrays.asList(Transferable.VALID_IMAGE_EXTENSIONS).contains(extension)
|| Arrays.asList(Transferable.WELL_KNOWN_EXTENSIONS).contains(extension)) {
return Decision.MUST;
} else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension)
|| Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) {
return Decision.SHOULD;
} else {
return Decision.NEVER;
@ -709,17 +806,17 @@ public class Message extends AbstractEntity {
public int height = 0;
}
public void setAxolotlFingerprint(String fingerprint) {
public void setFingerprint(String fingerprint) {
this.axolotlFingerprint = fingerprint;
}
public String getAxolotlFingerprint() {
public String getFingerprint() {
return axolotlFingerprint;
}
public boolean isTrusted() {
XmppAxolotlSession.Trust t = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
return t != null && t.trusted();
FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
return s != null && s.isTrusted();
}
private int getPreviousEncryption() {

View file

@ -3,10 +3,9 @@ package eu.siacs.conversations.entities;
import android.annotation.SuppressLint;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.xmpp.forms.Data;
@ -18,6 +17,8 @@ import eu.siacs.conversations.xmpp.pep.Avatar;
@SuppressLint("DefaultLocale")
public class MucOptions {
private boolean mAutoPushConfiguration = true;
public Account getAccount() {
return this.conversation.getAccount();
}
@ -26,6 +27,27 @@ public class MucOptions {
this.self = user;
}
public void changeAffiliation(Jid jid, Affiliation affiliation) {
User user = findUserByRealJid(jid);
synchronized (users) {
if (user != null && user.getRole() == Role.NONE) {
users.remove(user);
if (affiliation.ranks(Affiliation.MEMBER)) {
user.affiliation = affiliation;
users.add(user);
}
}
}
}
public void flagNoAutoPushConfiguration() {
mAutoPushConfiguration = false;
}
public boolean autoPushConfiguration() {
return mAutoPushConfiguration;
}
public enum Affiliation {
OWNER("owner", 4, R.string.owner),
ADMIN("admin", 3, R.string.admin),
@ -91,22 +113,27 @@ public class MucOptions {
}
}
public static final int ERROR_NO_ERROR = 0;
public static final int ERROR_NICK_IN_USE = 1;
public static final int ERROR_UNKNOWN = 2;
public static final int ERROR_PASSWORD_REQUIRED = 3;
public static final int ERROR_BANNED = 4;
public static final int ERROR_MEMBERS_ONLY = 5;
public static final int ERROR_NO_RESPONSE = 6;
public enum Error {
NO_RESPONSE,
SERVER_NOT_FOUND,
NONE,
NICK_IN_USE,
PASSWORD_REQUIRED,
BANNED,
MEMBERS_ONLY,
KICKED,
SHUTDOWN,
UNKNOWN
}
public static final int KICKED_FROM_ROOM = 9;
public static final String STATUS_CODE_ROOM_CONFIG_CHANGED = "104";
public static final String STATUS_CODE_SELF_PRESENCE = "110";
public static final String STATUS_CODE_ROOM_CREATED = "201";
public static final String STATUS_CODE_BANNED = "301";
public static final String STATUS_CODE_CHANGED_NICK = "303";
public static final String STATUS_CODE_KICKED = "307";
public static final String STATUS_CODE_LOST_MEMBERSHIP = "321";
public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
public static final String STATUS_CODE_SHUTDOWN = "332";
private interface OnEventListener {
void onSuccess();
@ -118,10 +145,10 @@ public class MucOptions {
}
public static class User {
public static class User implements Comparable<User> {
private Role role = Role.NONE;
private Affiliation affiliation = Affiliation.NONE;
private Jid jid;
private Jid realJid;
private Jid fullJid;
private long pgpKeyId = 0;
private Avatar avatar;
@ -133,15 +160,11 @@ public class MucOptions {
}
public String getName() {
return this.fullJid.getResourcepart();
return fullJid == null ? null : fullJid.getResourcepart();
}
public void setJid(Jid jid) {
this.jid = jid;
}
public Jid getJid() {
return this.jid;
public void setRealJid(Jid jid) {
this.realJid = jid != null ? jid.toBareJid() : null;
}
public Role getRole() {
@ -149,6 +172,10 @@ public class MucOptions {
}
public void setRole(String role) {
if (role == null) {
this.role = Role.NONE;
return;
}
role = role.toLowerCase();
switch (role) {
case "moderator":
@ -166,26 +193,15 @@ public class MucOptions {
}
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
} else if (!(other instanceof User)) {
return false;
} else {
User o = (User) other;
return getName() != null && getName().equals(o.getName())
&& jid != null && jid.equals(o.jid)
&& affiliation == o.affiliation
&& role == o.role;
}
}
public Affiliation getAffiliation() {
return this.affiliation;
}
public void setAffiliation(String affiliation) {
if (affiliation == null) {
this.affiliation = Affiliation.NONE;
return;
}
affiliation = affiliation.toLowerCase();
switch (affiliation) {
case "admin":
@ -214,7 +230,13 @@ public class MucOptions {
}
public Contact getContact() {
return getAccount().getRoster().getContactFromRoster(getJid());
if (fullJid != null) {
return getAccount().getRoster().getContactFromRoster(realJid);
} else if (realJid != null){
return getAccount().getRoster().getContact(realJid);
} else {
return null;
}
}
public boolean setAvatar(Avatar avatar) {
@ -237,15 +259,74 @@ public class MucOptions {
public Jid getFullJid() {
return fullJid;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
if (role != user.role) return false;
if (affiliation != user.affiliation) return false;
if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
return false;
return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
}
@Override
public int hashCode() {
int result = role != null ? role.hashCode() : 0;
result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "[fulljid:"+String.valueOf(fullJid)+",realjid:"+String.valueOf(realJid)+",affiliation"+affiliation.toString()+"]";
}
public boolean realJidMatchesAccount() {
return realJid != null && realJid.equals(options.account.getJid().toBareJid());
}
@Override
public int compareTo(User another) {
if (another.getAffiliation().outranks(getAffiliation())) {
return 1;
} else if (getAffiliation().outranks(another.getAffiliation())) {
return -1;
} else {
return getComparableName().compareToIgnoreCase(another.getComparableName());
}
}
private String getComparableName() {
Contact contact = getContact();
if (contact != null) {
return contact.getDisplayName();
} else {
String name = getName();
return name == null ? "" : name;
}
}
public Jid getRealJid() {
return realJid;
}
}
private Account account;
private final Map<String, User> users = Collections.synchronizedMap(new LinkedHashMap<String, User>());
private List<String> features = new ArrayList<>();
private final Set<User> users = new HashSet<>();
private final List<String> features = new ArrayList<>();
private Data form = new Data();
private Conversation conversation;
private boolean isOnline = false;
private int error = ERROR_NO_RESPONSE;
private Error error = Error.NONE;
public OnRenameListener onRenameListener = null;
private User self;
private String subject = null;
@ -282,7 +363,9 @@ public class MucOptions {
}
public boolean participating() {
return !online() || self.getRole().ranks(Role.PARTICIPANT);
return !online()
|| self.getRole().ranks(Role.PARTICIPANT)
|| hasFeature("muc_unmoderated");
}
public boolean membersOnly() {
@ -306,24 +389,106 @@ public class MucOptions {
return hasFeature("muc_moderated");
}
public User deleteUser(String name) {
return this.users.remove(name);
public User deleteUser(Jid jid) {
User user = findUserByFullJid(jid);
if (user != null) {
synchronized (users) {
users.remove(user);
boolean realJidInMuc = false;
for (User u : users) {
if (user.realJid != null && user.realJid.equals(u.realJid)) {
realJidInMuc = true;
break;
}
}
boolean self = user.realJid != null && user.realJid.equals(account.getJid().toBareJid());
if (membersOnly()
&& nonanonymous()
&& user.affiliation.ranks(Affiliation.MEMBER)
&& user.realJid != null
&& !realJidInMuc
&& !self) {
user.role = Role.NONE;
user.avatar = null;
user.fullJid = null;
users.add(user);
}
}
}
return user;
}
public void addUser(User user) {
this.users.put(user.getName(), user);
public void updateUser(User user) {
User old;
if (user.fullJid == null && user.realJid != null) {
old = findUserByRealJid(user.realJid);
if (old != null) {
if (old.fullJid != null) {
return; //don't add. user already exists
} else {
synchronized (users) {
users.remove(old);
}
}
}
} else if (user.realJid != null) {
old = findUserByRealJid(user.realJid);
synchronized (users) {
if (old != null && old.fullJid == null) {
users.remove(old);
}
}
}
old = findUserByFullJid(user.getFullJid());
synchronized (this.users) {
if (old != null) {
users.remove(old);
}
if ((!membersOnly() || user.getAffiliation().ranks(Affiliation.MEMBER))
&& user.getAffiliation().outranks(Affiliation.OUTCAST)){
this.users.add(user);
}
}
}
public User findUser(String name) {
return this.users.get(name);
public User findUserByFullJid(Jid jid) {
if (jid == null) {
return null;
}
synchronized (users) {
for (User user : users) {
if (jid.equals(user.getFullJid())) {
return user;
}
}
}
return null;
}
public boolean isUserInRoom(String name) {
return findUser(name) != null;
private User findUserByRealJid(Jid jid) {
if (jid == null) {
return null;
}
synchronized (users) {
for (User user : users) {
if (jid.equals(user.realJid)) {
return user;
}
}
}
return null;
}
public void setError(int error) {
this.isOnline = isOnline && error == ERROR_NO_ERROR;
public boolean isContactInRoom(Contact contact) {
return findUserByRealJid(contact.getJid().toBareJid()) != null;
}
public boolean isUserInRoom(Jid jid) {
return findUserByFullJid(jid) != null;
}
public void setError(Error error) {
this.isOnline = isOnline && error == Error.NONE;
this.error = error;
}
@ -332,32 +497,53 @@ public class MucOptions {
}
public ArrayList<User> getUsers() {
return new ArrayList<>(users.values());
return getUsers(true);
}
public ArrayList<User> getUsers(boolean includeOffline) {
synchronized (users) {
if (includeOffline) {
return new ArrayList<>(users);
} else {
ArrayList<User> onlineUsers = new ArrayList<>();
for (User user : users) {
if (user.getRole().ranks(Role.PARTICIPANT)) {
onlineUsers.add(user);
}
}
return onlineUsers;
}
}
}
public List<User> getUsers(int max) {
ArrayList<User> users = new ArrayList<>();
int i = 1;
for(User user : this.users.values()) {
users.add(user);
if (i >= max) {
break;
} else {
++i;
ArrayList<User> subset = new ArrayList<>();
HashSet<Jid> jids = new HashSet<>();
jids.add(account.getJid().toBareJid());
synchronized (users) {
for(User user : users) {
if (user.getRealJid() == null || jids.add(user.getRealJid())) {
subset.add(user);
}
if (subset.size() >= max) {
break;
}
}
}
return users;
return subset;
}
public int getUserCount() {
return this.users.size();
synchronized (users) {
return users.size();
}
}
public String getProposedNick() {
if (conversation.getBookmark() != null
&& conversation.getBookmark().getNick() != null
&& !conversation.getBookmark().getNick().isEmpty()) {
return conversation.getBookmark().getNick();
&& !conversation.getBookmark().getNick().trim().isEmpty()) {
return conversation.getBookmark().getNick().trim();
} else if (!conversation.getJid().isBareJid()) {
return conversation.getJid().getResourcepart();
} else {
@ -377,7 +563,7 @@ public class MucOptions {
return this.isOnline;
}
public int getError() {
public Error getError() {
return this.error;
}
@ -386,8 +572,10 @@ public class MucOptions {
}
public void setOffline() {
this.users.clear();
this.error = ERROR_NO_RESPONSE;
synchronized (users) {
this.users.clear();
}
this.error = Error.NO_RESPONSE;
this.isOnline = false;
}
@ -404,13 +592,13 @@ public class MucOptions {
}
public String createNameFromParticipants() {
if (users.size() >= 2) {
if (getUserCount() >= 2) {
List<String> names = new ArrayList<>();
for (User user : getUsers(5)) {
Contact contact = user.getContact();
if (contact != null && !contact.getDisplayName().isEmpty()) {
names.add(contact.getDisplayName().split("\\s+")[0]);
} else {
} else if (user.getName() != null){
names.add(user.getName());
}
}
@ -429,7 +617,7 @@ public class MucOptions {
public long[] getPgpKeyIds() {
List<Long> ids = new ArrayList<>();
for (User user : this.users.values()) {
for (User user : this.users) {
if (user.getPgpKeyId() != 0) {
ids.add(user.getPgpKeyId());
}
@ -443,18 +631,22 @@ public class MucOptions {
}
public boolean pgpKeysInUse() {
for (User user : this.users.values()) {
if (user.getPgpKeyId() != 0) {
return true;
synchronized (users) {
for (User user : users) {
if (user.getPgpKeyId() != 0) {
return true;
}
}
}
return false;
}
public boolean everybodyHasKeys() {
for (User user : this.users.values()) {
if (user.getPgpKeyId() == 0) {
return false;
synchronized (users) {
for (User user : users) {
if (user.getPgpKeyId() == 0) {
return false;
}
}
}
return true;
@ -468,9 +660,12 @@ public class MucOptions {
}
}
public Jid getTrueCounterpart(String name) {
User user = findUser(name);
return user == null ? null : user.getJid();
public Jid getTrueCounterpart(Jid jid) {
if (jid.equals(getSelf().getFullJid())) {
return account.getJid().toBareJid();
}
User user = findUserByFullJid(jid);
return user == null ? null : user.realJid;
}
public String getPassword() {
@ -495,4 +690,16 @@ public class MucOptions {
public Conversation getConversation() {
return this.conversation;
}
public List<Jid> getMembers() {
ArrayList<Jid> members = new ArrayList<>();
synchronized (users) {
for (User user : users) {
if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null) {
members.add(user.realJid);
}
}
}
return members;
}
}

View file

@ -0,0 +1,93 @@
package eu.siacs.conversations.entities;
import java.lang.Comparable;
import java.util.Locale;
import eu.siacs.conversations.xml.Element;
public class Presence implements Comparable {
public enum Status {
CHAT, ONLINE, AWAY, XA, DND, OFFLINE;
public String toShowString() {
switch(this) {
case CHAT: return "chat";
case AWAY: return "away";
case XA: return "xa";
case DND: return "dnd";
}
return null;
}
public static Status fromShowString(String show) {
if (show == null) {
return ONLINE;
} else {
switch (show.toLowerCase(Locale.US)) {
case "away":
return AWAY;
case "xa":
return XA;
case "dnd":
return DND;
case "chat":
return CHAT;
default:
return ONLINE;
}
}
}
}
private final Status status;
private ServiceDiscoveryResult disco;
private final String ver;
private final String hash;
private final String message;
private Presence(Status status, String ver, String hash, String message) {
this.status = status;
this.ver = ver;
this.hash = hash;
this.message = message;
}
public static Presence parse(String show, Element caps, String message) {
final String hash = caps == null ? null : caps.getAttribute("hash");
final String ver = caps == null ? null : caps.getAttribute("ver");
return new Presence(Status.fromShowString(show), ver, hash, message);
}
public int compareTo(Object other) {
return this.status.compareTo(((Presence)other).status);
}
public Status getStatus() {
return this.status;
}
public boolean hasCaps() {
return ver != null && hash != null;
}
public String getVer() {
return this.ver;
}
public String getHash() {
return this.hash;
}
public String getMessage() {
return this.message;
}
public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) {
this.disco = disco;
}
public ServiceDiscoveryResult getServiceDiscoveryResult() {
return disco;
}
}

View file

@ -0,0 +1,76 @@
package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
public class PresenceTemplate extends AbstractEntity {
public static final String TABELNAME = "presence_templates";
public static final String LAST_USED = "last_used";
public static final String MESSAGE = "message";
public static final String STATUS = "status";
private long lastUsed = 0;
private String statusMessage;
private Presence.Status status = Presence.Status.ONLINE;
public PresenceTemplate(Presence.Status status, String statusMessage) {
this.status = status;
this.statusMessage = statusMessage;
this.lastUsed = System.currentTimeMillis();
this.uuid = java.util.UUID.randomUUID().toString();
}
private PresenceTemplate() {
}
@Override
public ContentValues getContentValues() {
final String show = status.toShowString();
ContentValues values = new ContentValues();
values.put(LAST_USED, lastUsed);
values.put(MESSAGE, statusMessage);
values.put(STATUS, show == null ? "" : show);
values.put(UUID, uuid);
return values;
}
public static PresenceTemplate fromCursor(Cursor cursor) {
PresenceTemplate template = new PresenceTemplate();
template.uuid = cursor.getString(cursor.getColumnIndex(UUID));
template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED));
template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE));
template.status = Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS)));
return template;
}
public Presence.Status getStatus() {
return status;
}
public String getStatusMessage() {
return statusMessage;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PresenceTemplate template = (PresenceTemplate) o;
if (statusMessage != null ? !statusMessage.equals(template.statusMessage) : template.statusMessage != null)
return false;
return status == template.status;
}
@Override
public int hashCode() {
int result = statusMessage != null ? statusMessage.hashCode() : 0;
result = 31 * result + status.hashCode();
return result;
}
}

View file

@ -1,29 +1,27 @@
package eu.siacs.conversations.entities;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.List;
import java.util.Map;
import eu.siacs.conversations.xml.Element;
public class Presences {
private final Hashtable<String, Presence> presences = new Hashtable<>();
public static final int CHAT = -1;
public static final int ONLINE = 0;
public static final int AWAY = 1;
public static final int XA = 2;
public static final int DND = 3;
public static final int OFFLINE = 4;
private final Hashtable<String, Integer> presences = new Hashtable<>();
public Hashtable<String, Integer> getPresences() {
public Hashtable<String, Presence> getPresences() {
return this.presences;
}
public void updatePresence(String resource, int status) {
public void updatePresence(String resource, Presence presence) {
synchronized (this.presences) {
this.presences.put(resource, status);
this.presences.put(resource, presence);
}
}
@ -39,42 +37,27 @@ public class Presences {
}
}
public int getMostAvailableStatus() {
int status = OFFLINE;
public Presence.Status getShownStatus() {
Presence.Status status = Presence.Status.OFFLINE;
synchronized (this.presences) {
Iterator<Entry<String, Integer>> it = presences.entrySet().iterator();
while (it.hasNext()) {
Entry<String, Integer> entry = it.next();
if (entry.getValue() < status)
status = entry.getValue();
for(Presence p : presences.values()) {
if (p.getStatus() == Presence.Status.DND) {
return p.getStatus();
} else if (p.getStatus().compareTo(status) < 0){
status = p.getStatus();
}
}
}
return status;
}
public static int parseShow(Element show) {
if ((show == null) || (show.getContent() == null)) {
return Presences.ONLINE;
} else if (show.getContent().equals("away")) {
return Presences.AWAY;
} else if (show.getContent().equals("xa")) {
return Presences.XA;
} else if (show.getContent().equals("chat")) {
return Presences.CHAT;
} else if (show.getContent().equals("dnd")) {
return Presences.DND;
} else {
return Presences.OFFLINE;
}
}
public int size() {
synchronized (this.presences) {
return presences.size();
}
}
public String[] asStringArray() {
public String[] toResourceArray() {
synchronized (this.presences) {
final String[] presencesArray = new String[presences.size()];
presences.keySet().toArray(presencesArray);
@ -82,9 +65,70 @@ public class Presences {
}
}
public List<PresenceTemplate> asTemplates() {
synchronized (this.presences) {
ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
for(Presence p : presences.values()) {
if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
}
}
return templates;
}
}
public boolean has(String presence) {
synchronized (this.presences) {
return presences.containsKey(presence);
}
}
public List<String> getStatusMessages() {
ArrayList<String> messages = new ArrayList<>();
synchronized (this.presences) {
for(Presence presence : this.presences.values()) {
String message = presence.getMessage() == null ? null : presence.getMessage().trim();
if (message != null && !message.isEmpty() && !messages.contains(message)) {
messages.add(message);
}
}
}
return messages;
}
public boolean allOrNonSupport(String namespace) {
synchronized (this.presences) {
for(Presence presence : this.presences.values()) {
ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
if (disco == null || !disco.getFeatures().contains(namespace)) {
return false;
}
}
}
return true;
}
public Pair<Map<String, String>,Map<String,String>> toTypeAndNameMap() {
Map<String,String> typeMap = new HashMap<>();
Map<String,String> nameMap = new HashMap<>();
synchronized (this.presences) {
for(Map.Entry<String,Presence> presenceEntry : this.presences.entrySet()) {
String resource = presenceEntry.getKey();
Presence presence = presenceEntry.getValue();
ServiceDiscoveryResult serviceDiscoveryResult = presence == null ? null : presence.getServiceDiscoveryResult();
if (serviceDiscoveryResult != null && serviceDiscoveryResult.getIdentities().size() > 0) {
ServiceDiscoveryResult.Identity identity = serviceDiscoveryResult.getIdentities().get(0);
String type = identity.getType();
String name = identity.getName();
if (type != null) {
typeMap.put(resource,type);
}
if (name != null) {
nameMap.put(resource, name);
}
}
}
}
return new Pair(typeMap,nameMap);
}
}

View file

@ -9,7 +9,7 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class Roster {
final Account account;
final HashMap<String, Contact> contacts = new HashMap<>();
final HashMap<Jid, Contact> contacts = new HashMap<>();
private String version = null;
public Roster(Account account) {
@ -21,7 +21,7 @@ public class Roster {
return null;
}
synchronized (this.contacts) {
Contact contact = contacts.get(jid.toBareJid().toString());
Contact contact = contacts.get(jid.toBareJid());
if (contact != null && contact.showInRoster()) {
return contact;
} else {
@ -32,15 +32,13 @@ public class Roster {
public Contact getContact(final Jid jid) {
synchronized (this.contacts) {
final Jid bareJid = jid.toBareJid();
if (contacts.containsKey(bareJid.toString())) {
return contacts.get(bareJid.toString());
} else {
Contact contact = new Contact(bareJid);
if (!contacts.containsKey(jid.toBareJid())) {
Contact contact = new Contact(jid.toBareJid());
contact.setAccount(account);
contacts.put(bareJid.toString(), contact);
contacts.put(contact.getJid().toBareJid(), contact);
return contact;
}
return contacts.get(jid.toBareJid());
}
}
@ -80,7 +78,7 @@ public class Roster {
contact.setAccount(account);
contact.setOption(Contact.Options.IN_ROSTER);
synchronized (this.contacts) {
contacts.put(contact.getJid().toBareJid().toString(), contact);
contacts.put(contact.getJid().toBareJid(), contact);
}
}

View file

@ -0,0 +1,348 @@
package eu.siacs.conversations.entities;
import android.content.ContentValues;
import android.database.Cursor;
import android.util.Base64;
import java.io.UnsupportedEncodingException;
import java.lang.Comparable;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.forms.Data;
import eu.siacs.conversations.xmpp.forms.Field;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class ServiceDiscoveryResult {
public static final String TABLENAME = "discovery_results";
public static final String HASH = "hash";
public static final String VER = "ver";
public static final String RESULT = "result";
protected static String blankNull(String s) {
return s == null ? "" : s;
}
public static class Identity implements Comparable {
protected final String category;
protected final String type;
protected final String lang;
protected final String name;
public Identity(final String category, final String type, final String lang, final String name) {
this.category = category;
this.type = type;
this.lang = lang;
this.name = name;
}
public Identity(final Element el) {
this(
el.getAttribute("category"),
el.getAttribute("type"),
el.getAttribute("xml:lang"),
el.getAttribute("name")
);
}
public Identity(final JSONObject o) {
this(
o.optString("category", null),
o.optString("type", null),
o.optString("lang", null),
o.optString("name", null)
);
}
public String getCategory() {
return this.category;
}
public String getType() {
return this.type;
}
public String getLang() {
return this.lang;
}
public String getName() {
return this.name;
}
public int compareTo(Object other) {
Identity o = (Identity)other;
int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory()));
if(r == 0) {
r = blankNull(this.getType()).compareTo(blankNull(o.getType()));
}
if(r == 0) {
r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
}
if(r == 0) {
r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
}
return r;
}
public JSONObject toJSON() {
try {
JSONObject o = new JSONObject();
o.put("category", this.getCategory());
o.put("type", this.getType());
o.put("lang", this.getLang());
o.put("name", this.getName());
return o;
} catch(JSONException e) {
return null;
}
}
}
protected final String hash;
protected final byte[] ver;
protected final List<Identity> identities;
protected final List<String> features;
protected final List<Data> forms;
public ServiceDiscoveryResult(final IqPacket packet) {
this.identities = new ArrayList<>();
this.features = new ArrayList<>();
this.forms = new ArrayList<>();
this.hash = "sha-1"; // We only support sha-1 for now
final List<Element> elements = packet.query().getChildren();
for (final Element element : elements) {
if (element.getName().equals("identity")) {
Identity id = new Identity(element);
if (id.getType() != null && id.getCategory() != null) {
identities.add(id);
}
} else if (element.getName().equals("feature")) {
if (element.getAttribute("var") != null) {
features.add(element.getAttribute("var"));
}
} else if (element.getName().equals("x") && "jabber:x:data".equals(element.getAttribute("xmlns"))) {
forms.add(Data.parse(element));
}
}
this.ver = this.mkCapHash();
}
public ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
this.identities = new ArrayList<>();
this.features = new ArrayList<>();
this.forms = new ArrayList<>();
this.hash = hash;
this.ver = ver;
JSONArray identities = o.optJSONArray("identities");
if (identities != null) {
for (int i = 0; i < identities.length(); i++) {
this.identities.add(new Identity(identities.getJSONObject(i)));
}
}
JSONArray features = o.optJSONArray("features");
if (features != null) {
for (int i = 0; i < features.length(); i++) {
this.features.add(features.getString(i));
}
}
JSONArray forms = o.optJSONArray("forms");
if (forms != null) {
for(int i = 0; i < forms.length(); i++) {
this.forms.add(createFormFromJSONObject(forms.getJSONObject(i)));
}
}
}
private static Data createFormFromJSONObject(JSONObject o) {
Data data = new Data();
JSONArray names = o.names();
for(int i = 0; i < names.length(); ++i) {
try {
String name = names.getString(i);
JSONArray jsonValues = o.getJSONArray(name);
ArrayList<String> values = new ArrayList<>(jsonValues.length());
for(int j = 0; j < jsonValues.length(); ++j) {
values.add(jsonValues.getString(j));
}
data.put(name, values);
} catch (Exception e) {
e.printStackTrace();
}
}
return data;
}
private static JSONObject createJSONFromForm(Data data) {
JSONObject object = new JSONObject();
for(Field field : data.getFields()) {
try {
JSONArray jsonValues = new JSONArray();
for(String value : field.getValues()) {
jsonValues.put(value);
}
object.put(field.getFieldName(), jsonValues);
} catch(Exception e) {
e.printStackTrace();
}
}
try {
JSONArray jsonValues = new JSONArray();
jsonValues.put(data.getFormType());
object.put(Data.FORM_TYPE, jsonValues);
} catch(Exception e) {
e.printStackTrace();
}
return object;
}
public String getVer() {
return new String(Base64.encode(this.ver, Base64.DEFAULT)).trim();
}
public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
this(
cursor.getString(cursor.getColumnIndex(HASH)),
Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT),
new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT)))
);
}
public List<Identity> getIdentities() {
return this.identities;
}
public List<String> getFeatures() {
return this.features;
}
public boolean hasIdentity(String category, String type) {
for(Identity id : this.getIdentities()) {
if((category == null || id.getCategory().equals(category)) &&
(type == null || id.getType().equals(type))) {
return true;
}
}
return false;
}
public String getExtendedDiscoInformation(String formType, String name) {
for(Data form : this.forms) {
if (formType.equals(form.getFormType())) {
for(Field field: form.getFields()) {
if (name.equals(field.getFieldName())) {
return field.getValue();
}
}
}
}
return null;
}
protected byte[] mkCapHash() {
StringBuilder s = new StringBuilder();
List<Identity> identities = this.getIdentities();
Collections.sort(identities);
for(Identity id : identities) {
s.append(
blankNull(id.getCategory()) + "/" +
blankNull(id.getType()) + "/" +
blankNull(id.getLang()) + "/" +
blankNull(id.getName()) + "<"
);
}
List<String> features = this.getFeatures();
Collections.sort(features);
for (String feature : features) {
s.append(feature + "<");
}
Collections.sort(forms, new Comparator<Data>() {
@Override
public int compare(Data lhs, Data rhs) {
return lhs.getFormType().compareTo(rhs.getFormType());
}
});
for(Data form : forms) {
s.append(form.getFormType() + "<");
List<Field> fields = form.getFields();
Collections.sort(fields, new Comparator<Field>() {
@Override
public int compare(Field lhs, Field rhs) {
return lhs.getFieldName().compareTo(rhs.getFieldName());
}
});
for(Field field : fields) {
s.append(field.getFieldName()+"<");
List<String> values = field.getValues();
Collections.sort(values);
for(String value : values) {
s.append(value+"<");
}
}
}
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
return null;
}
try {
return md.digest(s.toString().getBytes("UTF-8"));
} catch(UnsupportedEncodingException e) {
return null;
}
}
public JSONObject toJSON() {
try {
JSONObject o = new JSONObject();
JSONArray ids = new JSONArray();
for(Identity id : this.getIdentities()) {
ids.put(id.toJSON());
}
o.put("identities", ids);
o.put("features", new JSONArray(this.getFeatures()));
JSONArray forms = new JSONArray();
for(Data data : this.forms) {
forms.put(createJSONFromForm(data));
}
o.put("forms", forms);
return o;
} catch(JSONException e) {
return null;
}
}
public ContentValues getContentValues() {
final ContentValues values = new ContentValues();
values.put(HASH, this.hash);
values.put(VER, getVer());
values.put(RESULT, this.toJSON().toString());
return values;
}
}

View file

@ -1,10 +1,13 @@
package eu.siacs.conversations.entities;
import java.util.Arrays;
import java.util.List;
public interface Transferable {
String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"};
String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"};
String[] WELL_KNOWN_EXTENSIONS = {"pdf","m4a","mp4"};
List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe");
List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg", "otr");
List<String> WELL_KNOWN_EXTENSIONS = Arrays.asList("pdf","m4a","mp4","3gp","aac","amr","mp3");
int STATUS_UNKNOWN = 0x200;
int STATUS_CHECKING = 0x201;

View file

@ -12,14 +12,18 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
public abstract class AbstractGenerator {
private final String[] FEATURES = {
"urn:xmpp:jingle:1",
"urn:xmpp:jingle:apps:file-transfer:3",
Content.Version.FT_3.getNamespace(),
Content.Version.FT_4.getNamespace(),
"urn:xmpp:jingle:transports:s5b:1",
"urn:xmpp:jingle:transports:ibb:1",
"http://jabber.org/protocol/muc",
@ -30,17 +34,24 @@ public abstract class AbstractGenerator {
"http://jabber.org/protocol/nick+notify",
"urn:xmpp:ping",
"jabber:iq:version",
"http://jabber.org/protocol/chatstates",
AxolotlService.PEP_DEVICE_LIST+"+notify"};
"http://jabber.org/protocol/chatstates"
};
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
"urn:xmpp:chat-markers:0",
"urn:xmpp:receipts"
};
private final String[] MESSAGE_CORRECTION_FEATURES = {
"urn:xmpp:message-correct:0"
};
private final String[] PRIVACY_SENSITIVE = {
"urn:xmpp:time" //XEP-0202: Entity Time leaks time zone
};
private final String[] OTR = {
"urn:xmpp:otr:0"
};
private String mVersion = null;
protected final String IDENTITY_NAME = "Conversations";
protected final String IDENTITY_TYPE = "phone";
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
protected XmppConnectionService mXmppConnectionService;
@ -56,12 +67,20 @@ public abstract class AbstractGenerator {
}
public String getIdentityName() {
return IDENTITY_NAME + " " + getIdentityVersion();
return mXmppConnectionService.getString(R.string.app_name) + " " + getIdentityVersion();
}
public String getIdentityType() {
if ("chromium".equals(android.os.Build.BRAND)) {
return "pc";
} else {
return mXmppConnectionService.getString(R.string.default_resource).toLowerCase();
}
}
public String getCapHash() {
StringBuilder s = new StringBuilder();
s.append("client/" + IDENTITY_TYPE + "//" + getIdentityName() + "<");
s.append("client/" + getIdentityType() + "//" + getIdentityName() + "<");
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-1");
@ -87,6 +106,18 @@ public abstract class AbstractGenerator {
if (mXmppConnectionService.confirmMessages()) {
features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
}
if (mXmppConnectionService.allowMessageCorrection()) {
features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES));
}
if (Config.supportOmemo()) {
features.add(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
}
if (!mXmppConnectionService.useTorToConnect()) {
features.addAll(Arrays.asList(PRIVACY_SENSITIVE));
}
if (Config.supportOtr()) {
features.addAll(Arrays.asList(OTR));
}
Collections.sort(features);
return features;
}

View file

@ -1,6 +1,7 @@
package eu.siacs.conversations.generator;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;
@ -9,13 +10,18 @@ import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.UUID;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
@ -44,7 +50,7 @@ public class IqGenerator extends AbstractGenerator {
query.setAttribute("node", request.query().getAttribute("node"));
final Element identity = query.addChild("identity");
identity.setAttribute("category", "client");
identity.setAttribute("type", IDENTITY_TYPE);
identity.setAttribute("type", getIdentityType());
identity.setAttribute("name", getIdentityName());
for (final String feature : getFeatures()) {
query.addChild("feature").setAttribute("var", feature);
@ -55,8 +61,26 @@ public class IqGenerator extends AbstractGenerator {
public IqPacket versionResponse(final IqPacket request) {
final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
Element query = packet.query("jabber:iq:version");
query.addChild("name").setContent(IDENTITY_NAME);
query.addChild("name").setContent(mXmppConnectionService.getString(R.string.app_name));
query.addChild("version").setContent(getIdentityVersion());
if ("chromium".equals(android.os.Build.BRAND)) {
query.addChild("os").setContent("Chrome OS");
} else{
query.addChild("os").setContent("Android");
}
return packet;
}
public IqPacket entityTimeResponse(IqPacket request) {
final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
Element time = packet.addChild("time","urn:xmpp:time");
final long now = System.currentTimeMillis();
time.addChild("utc").setContent(getTimestamp(now));
TimeZone ourTimezone = TimeZone.getDefault();
long offsetSeconds = ourTimezone.getOffset(now) / 1000;
long offsetMinutes = offsetSeconds % (60 * 60);
long offsetHours = offsetSeconds / (60 * 60);
time.addChild("tzo").setContent(String.format("%02d",offsetHours)+":"+String.format("%02d",offsetMinutes));
return packet;
}
@ -233,10 +257,14 @@ public class IqGenerator extends AbstractGenerator {
return iq;
}
public IqPacket generateSetBlockRequest(final Jid jid) {
public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) {
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
final Element block = iq.addChild("block", Xmlns.BLOCKING);
block.addChild("item").setAttribute("jid", jid.toBareJid().toString());
final Element item = block.addChild("item").setAttribute("jid", jid.toBareJid().toString());
if (reportSpam) {
item.addChild("report", "urn:xmpp:reporting:0").addChild("spam");
}
Log.d(Config.LOGTAG,iq.toString());
return iq;
}
@ -289,8 +317,8 @@ public class IqGenerator extends AbstractGenerator {
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD);
request.addChild("filename").setContent(file.getName());
Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD);
request.addChild("filename").setContent(convertFilename(file.getName()));
request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
if (mime != null) {
request.addChild("content-type").setContent(mime);
@ -298,13 +326,75 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
private static String convertFilename(String name) {
int pos = name.indexOf('.');
if (pos != -1) {
try {
UUID uuid = UUID.fromString(name.substring(0, pos));
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(uuid.getMostSignificantBits());
bb.putLong(uuid.getLeastSignificantBits());
return Base64.encodeToString(bb.array(), Base64.URL_SAFE) + name.substring(pos, name.length());
} catch (Exception e) {
return name;
}
} else {
return name;
}
}
public IqPacket generateCreateAccountWithCaptcha(Account account, String id, Data data) {
final IqPacket register = new IqPacket(IqPacket.TYPE.SET);
register.setFrom(account.getJid().toBareJid());
register.setTo(account.getServer());
register.setId(id);
register.query("jabber:iq:register").addChild(data);
Element query = register.query("jabber:iq:register");
if (data != null) {
query.addChild(data);
}
return register;
}
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
packet.setTo(appServer);
Element command = packet.addChild("command", "http://jabber.org/protocol/commands");
command.setAttribute("node","register-push-gcm");
command.setAttribute("action","execute");
Data data = new Data();
data.put("token", token);
data.put("device-id", deviceId);
data.submit();
command.addChild(data);
return packet;
}
public IqPacket enablePush(Jid jid, String node, String secret) {
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
Element enable = packet.addChild("enable","urn:xmpp:push:0");
enable.setAttribute("jid",jid.toString());
enable.setAttribute("node", node);
Data data = new Data();
data.setFormType("http://jabber.org/protocol/pubsub#publish-options");
data.put("secret",secret);
data.submit();
enable.addChild(data);
return packet;
}
public IqPacket queryAffiliation(Conversation conversation, String affiliation) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(conversation.getJid().toBareJid());
packet.query("http://jabber.org/protocol/muc#admin").addChild("item").setAttribute("affiliation",affiliation);
return packet;
}
public static Bundle defaultRoomConfiguration() {
Bundle options = new Bundle();
options.putString("muc#roomconfig_persistentroom", "1");
options.putString("muc#roomconfig_membersonly", "1");
options.putString("muc#roomconfig_publicroom", "0");
options.putString("muc#roomconfig_whois", "anyone");
return options;
}
}

View file

@ -9,8 +9,11 @@ import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.XmppConnectionService;
@ -20,6 +23,10 @@ import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
public class MessageGenerator extends AbstractGenerator {
public static final String OTR_FALLBACK_MESSAGE = "I would like to start a private (OTR encrypted) conversation but your client doesnt seem to support that";
private static final String OMEMO_FALLBACK_MESSAGE = "I sent you an OMEMO encrypted message but your client doesnt seem to support that. Find more information on https://conversations.im/omemo";
private static final String PGP_FALLBACK_MESSAGE = "I sent you a PGP encrypted message but your client doesnt seem to support that.";
public MessageGenerator(XmppConnectionService service) {
super(service);
}
@ -47,6 +54,9 @@ public class MessageGenerator extends AbstractGenerator {
}
packet.setFrom(account.getJid());
packet.setId(message.getUuid());
if (message.edited()) {
packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId());
}
return packet;
}
@ -65,10 +75,21 @@ public class MessageGenerator extends AbstractGenerator {
return null;
}
packet.setAxolotlMessage(axolotlMessage.toElement());
if (Config.supportUnencrypted() && !recipientSupportsOmemo(message)) {
packet.setBody(OMEMO_FALLBACK_MESSAGE);
}
packet.addChild("store", "urn:xmpp:hints");
packet.addChild("encryption","urn:xmpp:eme:0")
.setAttribute("name","OMEMO")
.setAttribute("namespace",AxolotlService.PEP_PREFIX);
return packet;
}
private static boolean recipientSupportsOmemo(Message message) {
Contact c = message.getContact();
return c != null && c.getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY);
}
public static void addMessageHints(MessagePacket packet) {
packet.addChild("private", "urn:xmpp:carbons:2");
packet.addChild("no-copy", "urn:xmpp:hints");
@ -91,6 +112,8 @@ public class MessageGenerator extends AbstractGenerator {
content = message.getBody();
}
packet.setBody(otrSession.transformSending(content)[0]);
packet.addChild("encryption","urn:xmpp:eme:0")
.setAttribute("namespace","urn:xmpp:otr:0");
return packet;
} catch (OtrException e) {
return null;
@ -113,12 +136,16 @@ public class MessageGenerator extends AbstractGenerator {
public MessagePacket generatePgpChat(Message message) {
MessagePacket packet = preparePacket(message);
packet.setBody("This is an XEP-0027 encrypted message");
if (Config.supportUnencrypted()) {
packet.setBody(PGP_FALLBACK_MESSAGE);
}
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
packet.addChild("x", "jabber:x:encrypted").setContent(message.getBody());
}
packet.addChild("encryption","urn:xmpp:eme:0")
.setAttribute("namespace","jabber:x:encrypted");
return packet;
}
@ -163,6 +190,10 @@ public class MessageGenerator extends AbstractGenerator {
packet.setFrom(conversation.getAccount().getJid());
Element x = packet.addChild("x", "jabber:x:conference");
x.setAttribute("jid", conversation.getJid().toBareJid().toString());
String password = conversation.getMucOptions().getPassword();
if (password != null) {
x.setAttribute("password",password);
}
return packet;
}

View file

@ -2,7 +2,7 @@ package eu.siacs.conversations.generator;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
@ -37,25 +37,18 @@ public class PresenceGenerator extends AbstractGenerator {
return subscription("subscribed", contact);
}
public PresencePacket selfPresence(Account account, int presence) {
public PresencePacket selfPresence(Account account, Presence.Status status) {
return selfPresence(account, status, true);
}
public PresencePacket selfPresence(Account account, Presence.Status status, boolean includePgpAnnouncement) {
PresencePacket packet = new PresencePacket();
switch(presence) {
case Presences.AWAY:
packet.addChild("show").setContent("away");
break;
case Presences.XA:
packet.addChild("show").setContent("xa");
break;
case Presences.CHAT:
packet.addChild("show").setContent("chat");
break;
case Presences.DND:
packet.addChild("show").setContent("dnd");
break;
if(status.toShowString() != null) {
packet.addChild("show").setContent(status.toShowString());
}
packet.setFrom(account.getJid());
String sig = account.getPgpSignature();
if (sig != null) {
if (includePgpAnnouncement && sig != null && mXmppConnectionService.getPgpEngine() != null) {
packet.addChild("x", "jabber:x:signed").setContent(sig);
}
String capHash = getCapHash();

View file

@ -23,6 +23,8 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.SSLSocketHelper;
import eu.siacs.conversations.utils.TLSSocketFactory;
public class HttpConnectionManager extends AbstractConnectionManager {
@ -63,7 +65,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
final X509TrustManager trustManager;
final HostnameVerifier hostnameVerifier;
if (interactive) {
trustManager = mXmppConnectionService.getMemorizingTrustManager();
trustManager = mXmppConnectionService.getMemorizingTrustManager().getInteractive();
hostnameVerifier = mXmppConnectionService
.getMemorizingTrustManager().wrapHostnameVerifier(
new StrictHostnameVerifier());
@ -76,18 +78,7 @@ public class HttpConnectionManager extends AbstractConnectionManager {
new StrictHostnameVerifier());
}
try {
final SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new X509TrustManager[]{trustManager},
mXmppConnectionService.getRNG());
final SSLSocketFactory sf = sc.getSocketFactory();
final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites(
sf.getSupportedCipherSuites());
if (cipherSuites.length > 0) {
sc.getDefaultSSLParameters().setCipherSuites(cipherSuites);
}
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
connection.setSSLSocketFactory(sf);
connection.setHostnameVerifier(hostnameVerifier);
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
@ -95,6 +86,6 @@ public class HttpConnectionManager extends AbstractConnectionManager {
}
public Proxy getProxy() throws IOException {
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getLocalHost(), 8118));
return new Proxy(Proxy.Type.HTTP, new InetSocketAddress(InetAddress.getByAddress(new byte[]{127,0,0,1}), 8118));
}
}

View file

@ -1,7 +1,5 @@
package eu.siacs.conversations.http;
import android.content.Intent;
import android.net.Uri;
import android.os.PowerManager;
import android.util.Log;
@ -10,12 +8,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.util.Arrays;
import java.util.concurrent.CancellationException;
import javax.net.ssl.HttpsURLConnection;
@ -31,6 +25,7 @@ import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.FileWriterException;
public class HttpDownloadConnection implements Transferable {
@ -89,12 +84,12 @@ public class HttpDownloadConnection implements Transferable {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
String extension;
if (Arrays.asList(VALID_CRYPTO_EXTENSIONS).contains(lastPart)) {
if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
extension = secondToLast;
} else {
extension = lastPart;
}
message.setRelativeFilePath(message.getUuid()+"."+extension);
message.setRelativeFilePath(message.getUuid() + "." + extension);
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
String reference = mUrl.getRef();
if (reference != null && reference.length() == 96) {
@ -125,35 +120,35 @@ public class HttpDownloadConnection implements Transferable {
} else {
message.setTransferable(null);
}
mXmppConnectionService.updateConversationUi();
mHttpConnectionManager.updateConversationUi(true);
}
private void finish() {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
mXmppConnectionService.sendBroadcast(intent);
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
message.setTransferable(null);
mHttpConnectionManager.finishConnection(this);
boolean notify = acceptedAutomatically && !message.isRead();
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
message.getConversation().getAccount().getPgpDecryptionService().add(message);
notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify);
}
mXmppConnectionService.updateConversationUi();
if (acceptedAutomatically) {
mHttpConnectionManager.updateConversationUi(true);
if (notify) {
mXmppConnectionService.getNotificationService().push(message);
}
}
private void changeStatus(int status) {
this.mStatus = status;
mXmppConnectionService.updateConversationUi();
mHttpConnectionManager.updateConversationUi(true);
}
private void showToastForException(Exception e) {
e.printStackTrace();
if (e instanceof java.net.UnknownHostException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
} else if (e instanceof java.net.ConnectException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
} else if (e instanceof FileWriterException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_write_file);
} else if (!(e instanceof CancellationException)) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
}
@ -172,21 +167,22 @@ public class HttpDownloadConnection implements Transferable {
long size;
try {
size = retrieveFileSize();
} catch (SSLHandshakeException e) {
} catch (Exception e) {
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
HttpDownloadConnection.this.acceptedAutomatically = false;
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
return;
} catch (IOException e) {
Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
if (interactive) {
showToastForException(e);
} else {
HttpDownloadConnection.this.acceptedAutomatically = false;
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
}
cancel();
return;
}
file.setExpectedSize(size);
if (mHttpConnectionManager.hasStoragePermission() && size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
if (mHttpConnectionManager.hasStoragePermission()
&& size <= mHttpConnectionManager.getAutoAcceptFileSize()
&& mXmppConnectionService.isDataSaverDisabled()) {
HttpDownloadConnection.this.acceptedAutomatically = true;
new Thread(new FileDownloader(interactive)).start();
} else {
@ -213,11 +209,13 @@ public class HttpDownloadConnection implements Transferable {
if (connection instanceof HttpsURLConnection) {
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
}
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.connect();
String contentLength = connection.getHeaderField("Content-Length");
connection.disconnect();
if (contentLength == null) {
throw new IOException();
throw new IOException("no content-length found in HEAD response");
}
return Long.parseLong(contentLength, 10);
} catch (IOException e) {
@ -251,6 +249,9 @@ public class HttpDownloadConnection implements Transferable {
} catch (Exception e) {
if (interactive) {
showToastForException(e);
} else {
HttpDownloadConnection.this.acceptedAutomatically = false;
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
}
cancel();
}
@ -272,14 +273,18 @@ public class HttpDownloadConnection implements Transferable {
}
connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
final boolean tryResume = file.exists() && file.getKey() == null;
long resumeSize = 0;
if (tryResume) {
Log.d(Config.LOGTAG,"http download trying resume");
long size = file.getSize();
connection.setRequestProperty("Range", "bytes="+size+"-");
resumeSize = file.getSize();
connection.setRequestProperty("Range", "bytes="+resumeSize+"-");
}
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.connect();
is = new BufferedInputStream(connection.getInputStream());
boolean serverResumed = "bytes".equals(connection.getHeaderField("Accept-Ranges"));
final String contentRange = connection.getHeaderField("Content-Range");
boolean serverResumed = tryResume && contentRange != null && contentRange.startsWith("bytes "+resumeSize+"-");
long transmitted = 0;
long expected = file.getExpectedSize();
if (tryResume && serverResumed) {
@ -287,31 +292,38 @@ public class HttpDownloadConnection implements Transferable {
transmitted = file.getSize();
updateProgress((int) ((((double) transmitted) / expected) * 100));
os = AbstractConnectionManager.createAppendedOutputStream(file);
if (os == null) {
throw new FileWriterException();
}
} else {
file.getParentFile().mkdirs();
file.createNewFile();
if (!file.exists() && !file.createNewFile()) {
throw new FileWriterException();
}
os = AbstractConnectionManager.createOutputStream(file, true);
}
int count = -1;
int count;
byte[] buffer = new byte[1024];
while ((count = is.read(buffer)) != -1) {
transmitted += count;
os.write(buffer, 0, count);
try {
os.write(buffer, 0, count);
} catch (IOException e) {
throw new FileWriterException();
}
updateProgress((int) ((((double) transmitted) / expected) * 100));
if (canceled) {
throw new CancellationException();
}
}
try {
os.flush();
} catch (IOException e) {
throw new FileWriterException();
}
} catch (CancellationException | IOException e) {
throw e;
} finally {
if (os != null) {
try {
os.flush();
} catch (final IOException ignored) {
}
}
FileBackend.close(os);
FileBackend.close(is);
wakeLock.release();
@ -328,7 +340,7 @@ public class HttpDownloadConnection implements Transferable {
public void updateProgress(int i) {
this.mProgress = i;
mXmppConnectionService.updateConversationUi();
mHttpConnectionManager.updateConversationUi(false);
}
@Override

View file

@ -1,8 +1,6 @@
package eu.siacs.conversations.http;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.PowerManager;
import android.util.Log;
import android.util.Pair;
@ -12,10 +10,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import javax.net.ssl.HttpsURLConnection;
@ -25,6 +20,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
@ -91,10 +87,10 @@ public class HttpUploadConnection implements Transferable {
this.canceled = true;
}
private void fail() {
private void fail(String errorMessage) {
mHttpConnectionManager.finishUploadConnection(this);
message.setTransferable(null);
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED, errorMessage);
FileBackend.close(mFileInputStream);
}
@ -115,7 +111,8 @@ public class HttpUploadConnection implements Transferable {
try {
pair = AbstractConnectionManager.createInputStream(file, true);
} catch (FileNotFoundException e) {
fail();
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not find file to upload - "+e.getMessage());
fail(e.getMessage());
return;
}
this.file.setExpectedSize(pair.second);
@ -134,15 +131,14 @@ public class HttpUploadConnection implements Transferable {
if (!canceled) {
new Thread(new FileUploader()).start();
}
return;
} catch (MalformedURLException e) {
fail();
//fall through
}
} else {
fail();
}
} else {
fail();
}
Log.d(Config.LOGTAG,account.getJid().toString()+": invalid response to slot request "+packet);
fail(IqParser.extractErrorMessage(packet));
}
});
message.setTransferable(this);
@ -176,15 +172,17 @@ public class HttpUploadConnection implements Transferable {
connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime);
connection.setRequestProperty("User-Agent",mXmppConnectionService.getIqGenerator().getIdentityName());
connection.setDoOutput(true);
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
connection.connect();
os = connection.getOutputStream();
transmitted = 0;
int count = -1;
int count;
byte[] buffer = new byte[4096];
while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
transmitted += count;
os.write(buffer, 0, count);
mXmppConnectionService.updateConversationUi();
mHttpConnectionManager.updateConversationUi(false);
}
os.flush();
os.close();
@ -196,9 +194,7 @@ public class HttpUploadConnection implements Transferable {
mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key));
}
mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl);
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
mXmppConnectionService.sendBroadcast(intent);
mXmppConnectionService.getFileBackend().updateMediaScanner(file);
message.setTransferable(null);
message.setCounterpart(message.getConversation().getJid().toBareJid());
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
@ -210,24 +206,26 @@ public class HttpUploadConnection implements Transferable {
@Override
public void error(int errorCode, Message object) {
fail();
Log.d(Config.LOGTAG,"pgp encryption failed");
fail("pgp encryption failed");
}
@Override
public void userInputRequried(PendingIntent pi, Message object) {
fail();
fail("pgp encryption failed");
}
});
} else {
mXmppConnectionService.resendMessage(message, delayed);
}
} else {
fail();
Log.d(Config.LOGTAG,"http upload failed because response code was "+code);
fail("http upload failed because response code was "+code);
}
} catch (IOException e) {
e.printStackTrace();
Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
fail();
fail(e.getMessage());
} finally {
FileBackend.close(mFileInputStream);
FileBackend.close(os);

View file

@ -1,17 +1,17 @@
package eu.siacs.conversations.parser;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.AbstractStanza;
public abstract class AbstractParser {
@ -21,46 +21,48 @@ public abstract class AbstractParser {
this.mXmppConnectionService = service;
}
public static Long getTimestamp(Element element, Long defaultValue) {
public static Long parseTimestamp(Element element, Long d) {
Element delay = element.findChild("delay","urn:xmpp:delay");
if (delay != null) {
String stamp = delay.getAttribute("stamp");
if (stamp != null) {
try {
return AbstractParser.parseTimestamp(delay.getAttribute("stamp")).getTime();
return AbstractParser.parseTimestamp(delay.getAttribute("stamp"));
} catch (ParseException e) {
return defaultValue;
return d;
}
}
}
return defaultValue;
return d;
}
protected long getTimestamp(Element packet) {
return getTimestamp(packet,System.currentTimeMillis());
public static long parseTimestamp(Element element) {
return parseTimestamp(element, System.currentTimeMillis());
}
public static Date parseTimestamp(String timestamp) throws ParseException {
public static long parseTimestamp(String timestamp) throws ParseException {
timestamp = timestamp.replace("Z", "+0000");
SimpleDateFormat dateFormat;
long ms;
if (timestamp.charAt(19) == '.' && timestamp.length() >= 25) {
String millis = timestamp.substring(19,timestamp.length() - 5);
try {
double fractions = Double.parseDouble("0" + millis);
ms = Math.round(1000 * fractions);
} catch (NumberFormatException e) {
ms = 0;
}
} else {
ms = 0;
}
timestamp = timestamp.substring(0,19)+timestamp.substring(timestamp.length() -5,timestamp.length());
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US);
return dateFormat.parse(timestamp);
return Math.min(dateFormat.parse(timestamp).getTime()+ms, System.currentTimeMillis());
}
protected void updateLastseen(final AbstractStanza packet, final Account account, final boolean presenceOverwrite) {
updateLastseen(getTimestamp(packet), account, packet.getFrom(), presenceOverwrite);
}
protected void updateLastseen(long timestamp, final Account account, final Jid from, final boolean presenceOverwrite) {
final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart();
protected void updateLastseen(final Account account, final Jid from) {
final Contact contact = account.getRoster().getContact(from);
if (timestamp >= contact.lastseen.time) {
contact.lastseen.time = timestamp;
if (!presence.isEmpty() && presenceOverwrite) {
contact.lastseen.presence = presence;
}
}
contact.setLastResource(from.isBareJid() ? "" : from.getResourcepart());
}
protected String avatarData(Element items) {
@ -70,4 +72,43 @@ public abstract class AbstractParser {
}
return item.findChildContent("data", "urn:xmpp:avatar:data");
}
public static MucOptions.User parseItem(Conversation conference, Element item) {
return parseItem(conference,item, null);
}
public static MucOptions.User parseItem(Conversation conference, Element item, Jid fullJid) {
final String local = conference.getJid().getLocalpart();
final String domain = conference.getJid().getDomainpart();
String affiliation = item.getAttribute("affiliation");
String role = item.getAttribute("role");
String nick = item.getAttribute("nick");
if (nick != null && fullJid == null) {
try {
fullJid = Jid.fromParts(local, domain, nick);
} catch (InvalidJidException e) {
fullJid = null;
}
}
Jid realJid = item.getAttributeAsJid("jid");
MucOptions.User user = new MucOptions.User(conference.getMucOptions(), fullJid);
user.setRealJid(realJid);
user.setAffiliation(affiliation);
user.setRole(role);
return user;
}
public static String extractErrorMessage(Element packet) {
final Element error = packet.findChild("error");
if (error != null && error.getChildren().size() > 0) {
final String text = error.findChildContent("text");
if (text != null && !text.trim().isEmpty()) {
return text;
} else {
return error.getChildren().get(0).getName().replace("-"," ");
}
} else {
return null;
}
}
}

View file

@ -6,7 +6,6 @@ import android.util.Log;
import android.util.Pair;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
@ -27,6 +26,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
@ -55,6 +55,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
final String name = item.getAttribute("name");
final String subscription = item.getAttribute("subscription");
final Contact contact = account.getRoster().getContact(jid);
boolean bothPre = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
if (!contact.getOption(Contact.Options.DIRTY_PUSH)) {
contact.setServerName(name);
contact.parseGroupsFromElement(item);
@ -70,6 +71,14 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
contact.parseSubscriptionFromElement(item);
}
}
boolean both = contact.getOption(Contact.Options.TO) && contact.getOption(Contact.Options.FROM);
if ((both != bothPre) && both) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": gained mutual presence subscription with "+contact.getJid());
AxolotlService axolotlService = account.getAxolotlService();
if (axolotlService != null) {
axolotlService.clearErrorsInFetchStatusMap(contact.getJid());
}
}
mXmppConnectionService.getAvatarService().clear(contact);
}
}
@ -131,7 +140,11 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
if(signedPreKeyPublic == null) {
return null;
}
return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
try {
return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
} catch (NumberFormatException e) {
return null;
}
}
public ECPublicKey signedPreKeyPublic(final Element bundle) {
@ -142,7 +155,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
try {
publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
} catch (InvalidKeyException | IllegalArgumentException e) {
} catch (Throwable e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage());
}
return publicKey;
@ -155,7 +168,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
try {
return Base64.decode(signedPreKeySignature.getContent(), Base64.DEFAULT);
} catch (IllegalArgumentException e) {
} catch (Throwable e) {
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : Invalid base64 in signedPreKeySignature");
return null;
}
@ -169,7 +182,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
try {
identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
} catch (InvalidKeyException | IllegalArgumentException e) {
} catch (Throwable e) {
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
}
return identityKey;
@ -196,13 +209,15 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
continue;
}
Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
Integer preKeyId = null;
try {
ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
final ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
preKeyRecords.put(preKeyId, preKeyPublic);
} catch (InvalidKeyException | IllegalArgumentException e) {
} catch (NumberFormatException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"could not parse preKeyId from preKey "+preKeyPublicElement.toString());
} catch (Throwable e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
continue;
}
}
return preKeyRecords;
@ -245,7 +260,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
Integer signedPreKeyId = signedPreKeyId(bundleElement);
byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
IdentityKey identityKey = identityKey(bundleElement);
if(signedPreKeyPublic == null || identityKey == null) {
if(signedPreKeyId == null || signedPreKeyPublic == null || identityKey == null) {
return null;
}
@ -269,6 +284,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
final boolean isGet = packet.getType() == IqPacket.TYPE.GET;
if (packet.getType() == IqPacket.TYPE.ERROR || packet.getType() == IqPacket.TYPE.TIMEOUT) {
return;
} else if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {
@ -304,9 +320,21 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
}
}
account.getBlocklist().addAll(jids);
if (packet.getType() == IqPacket.TYPE.SET) {
for(Jid jid : jids) {
Conversation conversation = mXmppConnectionService.find(account,jid);
if (conversation != null) {
mXmppConnectionService.markRead(conversation);
}
}
}
}
// Update the UI
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED);
if (packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null);
}
} else if (packet.hasChild("unblock", Xmlns.BLOCKING) &&
packet.fromServer(account) && packet.getType() == IqPacket.TYPE.SET) {
Log.d(Config.LOGTAG, "Received unblock update from server");
@ -327,19 +355,33 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
account.getBlocklist().removeAll(jids);
}
mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED);
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("open", "http://jabber.org/protocol/ibb")
|| packet.hasChild("data", "http://jabber.org/protocol/ibb")) {
|| packet.hasChild("data", "http://jabber.org/protocol/ibb")
|| packet.hasChild("close","http://jabber.org/protocol/ibb")) {
mXmppConnectionService.getJingleConnectionManager()
.deliverIbbPacket(account, packet);
} else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) {
final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(packet);
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("query","jabber:iq:version")) {
} else if (packet.hasChild("query","jabber:iq:version") && isGet) {
final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet);
mXmppConnectionService.sendIqPacket(account,response,null);
} else if (packet.hasChild("ping", "urn:xmpp:ping")) {
} else if (packet.hasChild("ping", "urn:xmpp:ping") && isGet) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT);
mXmppConnectionService.sendIqPacket(account, response, null);
} else if (packet.hasChild("time","urn:xmpp:time") && isGet) {
final IqPacket response;
if (mXmppConnectionService.useTorToConnect()) {
response = packet.generateResponse(IqPacket.TYPE.ERROR);
final Element error = response.addChild("error");
error.setAttribute("type","cancel");
error.addChild("not-allowed","urn:ietf:params:xml:ns:xmpp-stanzas");
} else {
response = mXmppConnectionService.getIqGenerator().entityTimeResponse(packet);
}
mXmppConnectionService.sendIqPacket(account,response, null);
} else {
if (packet.getType() == IqPacket.TYPE.GET || packet.getType() == IqPacket.TYPE.SET) {
final IqPacket response = packet.generateResponse(IqPacket.TYPE.ERROR);

View file

@ -1,16 +1,22 @@
package eu.siacs.conversations.parser;
import android.text.Html;
import android.util.Log;
import android.util.Pair;
import eu.siacs.conversations.crypto.PgpDecryptionService;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
@ -19,10 +25,13 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
@ -30,8 +39,10 @@ import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
public class MessageParser extends AbstractParser implements
OnMessagePacketReceived {
public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
private static final List<String> CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin","Adium","Trillian");
public MessageParser(XmppConnectionService service) {
super(service);
}
@ -45,7 +56,7 @@ public class MessageParser extends AbstractParser implements
conversation.setOutgoingChatState(state);
if (state == ChatState.ACTIVE || state == ChatState.COMPOSING) {
mXmppConnectionService.markRead(conversation);
account.activateGracePeriod();
activateGracePeriod(account);
}
return false;
} else {
@ -94,8 +105,16 @@ public class MessageParser extends AbstractParser implements
conversation.setSymmetricKey(CryptoHelper.hexToBytes(key));
return null;
}
if (clientMightSendHtml(conversation.getAccount(), from)) {
Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": received OTR message from bad behaving client. escaping HTML…");
body = Html.fromHtml(body).toString();
}
final OtrService otrService = conversation.getAccount().getOtrService();
Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED);
finishedMessage.setFingerprint(otrService.getFingerprint(otrSession.getRemotePublicKey()));
conversation.setLastReceivedOtrMessageId(null);
return finishedMessage;
} catch (Exception e) {
conversation.resetOtrSession();
@ -103,33 +122,58 @@ public class MessageParser extends AbstractParser implements
}
}
private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
Message finishedMessage = null;
AxolotlService service = conversation.getAccount().getAxolotlService();
XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage);
if(plaintextMessage != null) {
finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
private static boolean clientMightSendHtml(Account account, Jid from) {
String resource = from.getResourcepart();
if (resource == null) {
return false;
}
return finishedMessage;
Presence presence = account.getRoster().getContact(from).getPresences().getPresences().get(resource);
ServiceDiscoveryResult disco = presence == null ? null : presence.getServiceDiscoveryResult();
if (disco == null) {
return false;
}
return hasIdentityKnowForSendingHtml(disco.getIdentities());
}
private Message parsePGPChat(final Conversation conversation, String pgpEncrypted, int status) {
final Message message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
PgpDecryptionService pgpDecryptionService = conversation.getAccount().getPgpDecryptionService();
pgpDecryptionService.add(message);
return message;
private static boolean hasIdentityKnowForSendingHtml(List<ServiceDiscoveryResult.Identity> identities) {
for(ServiceDiscoveryResult.Identity identity : identities) {
if (identity.getName() != null) {
if (CLIENTS_SENDING_HTML_IN_OTR.contains(identity.getName())) {
return true;
}
}
}
return false;
}
private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status) {
AxolotlService service = conversation.getAccount().getAxolotlService();
XmppAxolotlMessage xmppAxolotlMessage;
try {
xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
} catch (Exception e) {
Log.d(Config.LOGTAG,conversation.getAccount().getJid().toBareJid()+": invalid omemo message received "+e.getMessage());
return null;
}
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage);
if(plaintextMessage != null) {
Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
finishedMessage.setFingerprint(plaintextMessage.getFingerprint());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
return finishedMessage;
} else {
return null;
}
}
private class Invite {
Jid jid;
String password;
Invite(Jid jid, String password) {
final Jid jid;
final String password;
final Contact inviter;
Invite(Jid jid, String password, Contact inviter) {
this.jid = jid;
this.password = password;
this.inviter = inviter;
}
public boolean execute(Account account) {
@ -138,7 +182,7 @@ public class MessageParser extends AbstractParser implements
if (!conversation.getMucOptions().online()) {
conversation.getMucOptions().setPassword(password);
mXmppConnectionService.databaseBackend.updateConversation(conversation);
mXmppConnectionService.joinMuc(conversation);
mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
mXmppConnectionService.updateConversationUi();
}
return true;
@ -147,18 +191,22 @@ public class MessageParser extends AbstractParser implements
}
}
private Invite extractInvite(Element message) {
private Invite extractInvite(Account account, Element message) {
Element x = message.findChild("x", "http://jabber.org/protocol/muc#user");
if (x != null) {
Element invite = x.findChild("invite");
if (invite != null) {
Element pw = x.findChild("password");
return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null);
Jid from = invite.getAttributeAsJid("from");
Contact contact = from == null ? null : account.getRoster().getContact(from);
return new Invite(message.getAttributeAsJid("from"), pw != null ? pw.getContent(): null, contact);
}
} else {
x = message.findChild("x","jabber:x:conference");
if (x != null) {
return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password"));
Jid from = message.getAttributeAsJid("from");
Contact contact = from == null ? null : account.getRoster().getContact(from);
return new Invite(x.getAttributeAsJid("jid"),x.getAttribute("password"),contact);
}
}
return null;
@ -167,7 +215,7 @@ public class MessageParser extends AbstractParser implements
private static String extractStanzaId(Element packet, Jid by) {
for(Element child : packet.getChildren()) {
if (child.getName().equals("stanza-id")
&& "urn:xmpp:sid:0".equals(child.getNamespace())
&& Xmlns.STANZA_IDS.equals(child.getNamespace())
&& by.equals(child.getAttributeAsJid("by"))) {
return child.getAttribute("id");
}
@ -197,7 +245,7 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateRosterUi();
}
} else {
} else if (mXmppConnectionService.isDataSaverDisabled()) {
mXmppConnectionService.fetchAvatar(account, avatar);
}
}
@ -212,9 +260,10 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.updateAccountUi();
}
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
Element item = items.findChild("item");
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list ("+deviceIds+") update from "+ from + ", processing...");
AxolotlService axolotlService = account.getAxolotlService();
axolotlService.registerDevices(from, deviceIds);
mXmppConnectionService.updateAccountUi();
@ -225,19 +274,15 @@ public class MessageParser extends AbstractParser implements
if (packet.getType() == MessagePacket.TYPE_ERROR) {
Jid from = packet.getFrom();
if (from != null) {
Element error = packet.findChild("error");
String text = error == null ? null : error.findChildContent("text");
if (text != null) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text);
} else if (error != null) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error);
}
Message message = mXmppConnectionService.markMessage(account,
from.toBareJid(),
packet.getId(),
Message.STATUS_SEND_FAILED);
if (message != null && message.getEncryption() == Message.ENCRYPTION_OTR) {
message.getConversation().endOtrIfNeeded();
Message.STATUS_SEND_FAILED,
extractErrorMessage(packet));
if (message != null) {
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
message.getConversation().endOtrIfNeeded();
}
}
}
return true;
@ -271,7 +316,7 @@ public class MessageParser extends AbstractParser implements
packet = f.first;
isForwarded = true;
serverMsgId = result.getAttribute("id");
query.incrementTotalCount();
query.incrementMessageCount();
} else if (query != null) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender");
return;
@ -292,17 +337,22 @@ public class MessageParser extends AbstractParser implements
}
if (timestamp == null) {
timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
timestamp = AbstractParser.parseTimestamp(original,AbstractParser.parseTimestamp(packet));
}
final String body = packet.getBody();
final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
final Element oob = packet.findChild("x", "jabber:x:oob");
final boolean isOob = oob!= null && body != null && body.equals(oob.findChildContent("url"));
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
final Jid to = packet.getTo();
final Jid from = packet.getFrom();
final String remoteMsgId = packet.getId();
boolean notify = false;
if (from == null) {
Log.d(Config.LOGTAG,"no from in: "+packet.toString());
@ -310,7 +360,7 @@ public class MessageParser extends AbstractParser implements
}
boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT;
boolean isProperlyAddressed = (to != null ) && (!to.isBareJid() || account.countPresences() == 1);
boolean isProperlyAddressed = (to != null ) && (!to.isBareJid() || account.countPresences() == 0);
boolean isMucStatusMessage = from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status");
if (packet.fromAccount(account)) {
status = Message.STATUS_SEND;
@ -320,20 +370,24 @@ public class MessageParser extends AbstractParser implements
counterpart = from;
}
Invite invite = extractInvite(packet);
Invite invite = extractInvite(account, packet);
if (invite != null && invite.execute(account)) {
return;
}
if (extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), packet)) {
if (!isTypeGroupChat
&& query == null
&& extractChatState(mXmppConnectionService.find(account, counterpart.toBareJid()), packet)) {
mXmppConnectionService.updateConversationUi();
}
if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat, query);
final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
if (isTypeGroupChat) {
if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
status = Message.STATUS_SEND_RECEIVED;
isCarbon = true; //not really carbon but received from another resource
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status)) {
return;
} else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
@ -347,29 +401,55 @@ public class MessageParser extends AbstractParser implements
status = Message.STATUS_RECEIVED;
}
}
Message message;
if (body != null && body.startsWith("?OTR")) {
if (!isForwarded && !isTypeGroupChat && isProperlyAddressed) {
final Message message;
if (body != null && body.startsWith("?OTR") && Config.supportOtr()) {
if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) {
message = parseOtrChat(body, from, remoteMsgId, conversation);
if (message == null) {
return;
}
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring OTR message from "+from+" isForwarded="+Boolean.toString(isForwarded)+", isProperlyAddressed="+Boolean.valueOf(isProperlyAddressed));
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
} else if (pgpEncrypted != null) {
message = parsePGPChat(conversation, pgpEncrypted, status);
} else if (axolotlEncrypted != null) {
message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
} else if (pgpEncrypted != null && Config.supportOpenPgp()) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null && Config.supportOmemo()) {
Jid origin;
if (conversationMultiMode) {
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
origin = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
if (origin == null) {
Log.d(Config.LOGTAG,"axolotl message in non anonymous conference received");
return;
}
} else {
origin = from;
}
message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status);
if (message == null) {
return;
}
if (conversationMultiMode) {
message.setTrueCounterpart(origin);
}
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
if (serverMsgId == null) {
serverMsgId = extractStanzaId(packet, isTypeGroupChat ? conversation.getJid().toBareJid() : account.getServer());
final Jid by;
final boolean safeToExtract;
if (isTypeGroupChat) {
by = conversation.getJid().toBareJid();
safeToExtract = true; //conversation.getMucOptions().hasFeature(Xmlns.STANZA_IDS);
} else {
by = account.getJid().toBareJid();
safeToExtract = true; //account.getXmppConnection().getFeatures().stanzaIds();
}
if (safeToExtract) {
serverMsgId = extractStanzaId(packet, by);
}
}
message.setCounterpart(counterpart);
@ -377,19 +457,76 @@ public class MessageParser extends AbstractParser implements
message.setServerMsgId(serverMsgId);
message.setCarbon(isCarbon);
message.setTime(timestamp);
message.setOob(isOob);
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
if (conversation.getMode() == Conversation.MODE_MULTI) {
Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart());
message.setTrueCounterpart(trueCounterpart);
if (trueCounterpart != null) {
updateLastseen(timestamp, account, trueCounterpart, false);
if (conversationMultiMode) {
final Jid fallback = conversation.getMucOptions().getTrueCounterpart(counterpart);
Jid trueCounterpart;
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
trueCounterpart = message.getTrueCounterpart();
} else if (Config.PARSE_REAL_JID_FROM_MUC_MAM) {
trueCounterpart = getTrueCounterpart(query != null ? mucUserElement : null, fallback);
} else {
trueCounterpart = fallback;
}
if (trueCounterpart != null && trueCounterpart.toBareJid().equals(account.getJid().toBareJid())) {
status = isTypeGroupChat ? Message.STATUS_SEND_RECEIVED : Message.STATUS_SEND;
}
message.setStatus(status);
message.setTrueCounterpart(trueCounterpart);
if (!isTypeGroupChat) {
message.setType(Message.TYPE_PRIVATE);
}
} else {
updateLastseen(timestamp, account, packet.getFrom(), true);
updateLastseen(account, from);
}
if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) {
Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId,
counterpart,
message.getStatus() == Message.STATUS_RECEIVED,
message.isCarbon());
if (replacedMessage != null) {
final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
|| replacedMessage.getFingerprint().equals(message.getFingerprint());
final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
&& replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
final boolean duplicate = conversation.hasDuplicateMessage(message);
if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode) && !duplicate) {
Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'");
synchronized (replacedMessage) {
final String uuid = replacedMessage.getUuid();
replacedMessage.setUuid(UUID.randomUUID().toString());
replacedMessage.setBody(message.getBody());
replacedMessage.setEdited(replacedMessage.getRemoteMsgId());
replacedMessage.setRemoteMsgId(remoteMsgId);
replacedMessage.setEncryption(message.getEncryption());
if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) {
replacedMessage.markUnread();
}
mXmppConnectionService.updateMessage(replacedMessage, uuid);
mXmppConnectionService.getNotificationService().updateNotification(false);
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
sendMessageReceipts(account, packet);
}
if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) {
conversation.getAccount().getPgpDecryptionService().discard(replacedMessage);
conversation.getAccount().getPgpDecryptionService().decrypt(replacedMessage, false);
}
}
return;
} else {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received message correction but verification didn't check out");
}
}
}
long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
if (deletionDate != 0 && message.getTimeSent() < deletionDate) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": skipping message from "+message.getCounterpart().toString()+" because it was sent prior to our deletion date");
return;
}
boolean checkForDuplicates = query != null
|| (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay"))
|| message.getType() == Message.TYPE_PRIVATE;
@ -398,40 +535,38 @@ public class MessageParser extends AbstractParser implements
return;
}
conversation.add(message);
if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) {
conversation.prepend(message);
} else {
conversation.add(message);
}
if (query == null || query.getWith() == null) { //either no mam or catchup
if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) {
mXmppConnectionService.markRead(conversation);
if (query == null) {
account.activateGracePeriod();
activateGracePeriod(account);
}
} else {
message.markUnread();
notify = true;
}
}
if (query != null) {
query.incrementMessageCount();
} else {
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
notify = conversation.getAccount().getPgpDecryptionService().decrypt(message, notify);
}
if (query == null) {
mXmppConnectionService.updateConversationUi();
}
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
ArrayList<String> receiptsNamespaces = new ArrayList<>();
if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
receiptsNamespaces.add("urn:xmpp:chat-markers:0");
}
if (packet.hasChild("request", "urn:xmpp:receipts")) {
receiptsNamespaces.add("urn:xmpp:receipts");
}
if (receiptsNamespaces.size() > 0) {
MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
packet,
receiptsNamespaces,
packet.getType());
mXmppConnectionService.sendMessagePacket(account, receipt);
}
if (mXmppConnectionService.confirmMessages()
&& message.trusted()
&& remoteMsgId != null
&& !isForwarded
&& !isTypeGroupChat) {
sendMessageReceipts(account, packet);
}
if (message.getStatus() == Message.STATUS_RECEIVED
@ -441,13 +576,11 @@ public class MessageParser extends AbstractParser implements
conversation.endOtrIfNeeded();
}
if (message.getEncryption() == Message.ENCRYPTION_NONE || mXmppConnectionService.saveEncryptedMessages()) {
mXmppConnectionService.databaseBackend.createMessage(message);
}
mXmppConnectionService.databaseBackend.createMessage(message);
final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager();
if (message.trusted() && message.treatAsDownloadable() != Message.Decision.NEVER && manager.getAutoAcceptFileSize() > 0) {
manager.createNewDownloadConnection(message);
} else if (!message.isRead()) {
} else if (notify) {
if (query == null) {
mXmppConnectionService.getNotificationService().push(message);
} else if (query.getWith() == null) { // mam catchup
@ -455,8 +588,8 @@ public class MessageParser extends AbstractParser implements
}
}
} else if (!packet.hasChild("body")){ //no body
Conversation conversation = mXmppConnectionService.find(account, from.toBareJid());
if (isTypeGroupChat) {
Conversation conversation = mXmppConnectionService.find(account, from.toBareJid());
if (packet.hasChild("subject")) {
if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
@ -472,18 +605,46 @@ public class MessageParser extends AbstractParser implements
return;
}
}
if (conversation != null && isMucStatusMessage) {
for (Element child : mucUserElement.getChildren()) {
if (child.getName().equals("status")
&& MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED.equals(child.getAttribute("code"))) {
mXmppConnectionService.fetchConferenceConfiguration(conversation);
}
if (conversation != null && mucUserElement != null && from.isBareJid()) {
for (Element child : mucUserElement.getChildren()) {
if ("status".equals(child.getName())) {
try {
int code = Integer.parseInt(child.getAttribute("code"));
if ((code >= 170 && code <= 174) || (code >= 102 && code <= 104)) {
mXmppConnectionService.fetchConferenceConfiguration(conversation);
break;
}
} catch (Exception e) {
//ignored
}
} else if ("item".equals(child.getName())) {
MucOptions.User user = AbstractParser.parseItem(conversation,child);
Log.d(Config.LOGTAG,account.getJid()+": changing affiliation for "
+user.getRealJid()+" to "+user.getAffiliation()+" in "
+conversation.getJid().toBareJid());
if (!user.realJidMatchesAccount()) {
conversation.getMucOptions().updateUser(user);
mXmppConnectionService.getAvatarService().clear(conversation);
mXmppConnectionService.updateMucRosterUi();
mXmppConnectionService.updateConversationUi();
if (!user.getAffiliation().ranks(MucOptions.Affiliation.MEMBER)) {
Jid jid = user.getRealJid();
List<Jid> cryptoTargets = conversation.getAcceptedCryptoTargets();
if (cryptoTargets.remove(user.getRealJid())) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": removed "+jid+" from crypto targets of "+conversation.getName());
conversation.setAcceptedCryptoTargets(cryptoTargets);
mXmppConnectionService.updateConversation(conversation);
}
}
}
}
}
}
}
Element received = packet.findChild("received", "urn:xmpp:chat-markers:0");
if (received == null) {
received = packet.findChild("received", "urn:xmpp:receipts");
@ -499,7 +660,6 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.markRead(conversation);
}
} else {
updateLastseen(timestamp, account, packet.getFrom(), true);
final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), displayed.getAttribute("id"), Message.STATUS_SEND_DISPLAYED);
Message message = displayedMessage == null ? null : displayedMessage.prev();
while (message != null
@ -511,9 +671,9 @@ public class MessageParser extends AbstractParser implements
}
}
Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event");
Element event = original.findChild("event", "http://jabber.org/protocol/pubsub#event");
if (event != null) {
parseEvent(event, from, account);
parseEvent(event, original.getFrom(), account);
}
String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick");
@ -522,4 +682,35 @@ public class MessageParser extends AbstractParser implements
contact.setPresenceName(nick);
}
}
private static Jid getTrueCounterpart(Element mucUserElement, Jid fallback) {
final Element item = mucUserElement == null ? null : mucUserElement.findChild("item");
Jid result = item == null ? null : item.getAttributeAsJid("jid");
return result != null ? result : fallback;
}
private void sendMessageReceipts(Account account, MessagePacket packet) {
ArrayList<String> receiptsNamespaces = new ArrayList<>();
if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
receiptsNamespaces.add("urn:xmpp:chat-markers:0");
}
if (packet.hasChild("request", "urn:xmpp:receipts")) {
receiptsNamespaces.add("urn:xmpp:receipts");
}
if (receiptsNamespaces.size() > 0) {
MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
packet,
receiptsNamespaces,
packet.getType());
mXmppConnectionService.sendMessagePacket(account, receipt);
}
}
private static SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm:ss");
private void activateGracePeriod(Account account) {
long duration = mXmppConnectionService.getPreferences().getLong("race_period_length", 144) * 1000;
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": activating grace period till "+TIME_FORMAT.format(new Date(System.currentTimeMillis() + duration)));
account.activateGracePeriod(duration);
}
}

View file

@ -2,6 +2,7 @@ package eu.siacs.conversations.parser;
import android.util.Log;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
@ -13,7 +14,8 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
@ -36,10 +38,9 @@ public class PresenceParser extends AbstractParser implements
boolean before = mucOptions.online();
int count = mucOptions.getUserCount();
final List<MucOptions.User> tileUserBefore = mucOptions.getUsers(5);
processConferencePresence(packet, mucOptions);
processConferencePresence(packet, conversation);
final List<MucOptions.User> tileUserAfter = mucOptions.getUsers(5);
if (!tileUserAfter.equals(tileUserBefore)) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": update tiles for "+conversation.getName());
mXmppConnectionService.getAvatarService().clear(mucOptions);
}
if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUserCount())) {
@ -50,7 +51,8 @@ public class PresenceParser extends AbstractParser implements
}
}
private void processConferencePresence(PresencePacket packet, MucOptions mucOptions) {
private void processConferencePresence(PresencePacket packet, Conversation conversation) {
MucOptions mucOptions = conversation.getMucOptions();
final Jid from = packet.getFrom();
if (!from.isBareJid()) {
final String type = packet.getAttribute("type");
@ -61,11 +63,8 @@ public class PresenceParser extends AbstractParser implements
if (x != null) {
Element item = x.findChild("item");
if (item != null && !from.isBareJid()) {
mucOptions.setError(MucOptions.ERROR_NO_ERROR);
MucOptions.User user = new MucOptions.User(mucOptions,from);
user.setAffiliation(item.getAttribute("affiliation"));
user.setRole(item.getAttribute("role"));
user.setJid(item.getAttributeAsJid("jid"));
mucOptions.setError(MucOptions.Error.NONE);
MucOptions.User user = parseItem(conversation, item, from);
if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) {
mucOptions.setOnline();
mucOptions.setSelf(user);
@ -76,7 +75,16 @@ public class PresenceParser extends AbstractParser implements
mucOptions.mNickChangingInProgress = false;
}
} else {
mucOptions.addUser(user);
mucOptions.updateUser(user);
}
if (codes.contains(MucOptions.STATUS_CODE_ROOM_CREATED) && mucOptions.autoPushConfiguration()) {
Log.d(Config.LOGTAG,mucOptions.getAccount().getJid().toBareJid()
+": room '"
+mucOptions.getConversation().getJid().toBareJid()
+"' created. pushing default configuration");
mXmppConnectionService.pushConferenceConfiguration(mucOptions.getConversation(),
IqGenerator.defaultRoomConfiguration(),
null);
}
if (mXmppConnectionService.getPgpEngine() != null) {
Element signed = packet.findChild("x", "jabber:x:signed");
@ -95,7 +103,7 @@ public class PresenceParser extends AbstractParser implements
if (user.setAvatar(avatar)) {
mXmppConnectionService.getAvatarService().clear(user);
}
} else {
} else if (mXmppConnectionService.isDataSaverDisabled()) {
mXmppConnectionService.fetchAvatar(mucOptions.getAccount(), avatar);
}
}
@ -107,17 +115,25 @@ public class PresenceParser extends AbstractParser implements
if (codes.contains(MucOptions.STATUS_CODE_CHANGED_NICK)) {
mucOptions.mNickChangingInProgress = true;
} else if (codes.contains(MucOptions.STATUS_CODE_KICKED)) {
mucOptions.setError(MucOptions.KICKED_FROM_ROOM);
mucOptions.setError(MucOptions.Error.KICKED);
} else if (codes.contains(MucOptions.STATUS_CODE_BANNED)) {
mucOptions.setError(MucOptions.ERROR_BANNED);
mucOptions.setError(MucOptions.Error.BANNED);
} else if (codes.contains(MucOptions.STATUS_CODE_LOST_MEMBERSHIP)) {
mucOptions.setError(MucOptions.ERROR_MEMBERS_ONLY);
mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
} else if (codes.contains(MucOptions.STATUS_CODE_AFFILIATION_CHANGE)) {
mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
} else if (codes.contains(MucOptions.STATUS_CODE_SHUTDOWN)) {
mucOptions.setError(MucOptions.Error.SHUTDOWN);
} else {
mucOptions.setError(MucOptions.ERROR_UNKNOWN);
mucOptions.setError(MucOptions.Error.UNKNOWN);
Log.d(Config.LOGTAG, "unknown error in conference: " + packet);
}
} else if (!from.isBareJid()){
MucOptions.User user = mucOptions.deleteUser(from.getResourcepart());
Element item = x.findChild("item");
if (item != null) {
mucOptions.updateUser(parseItem(conversation, item, from));
}
MucOptions.User user = mucOptions.deleteUser(from);
if (user != null) {
mXmppConnectionService.getAvatarService().clear(user);
}
@ -130,14 +146,14 @@ public class PresenceParser extends AbstractParser implements
mucOptions.onRenameListener.onFailure();
}
} else {
mucOptions.setError(MucOptions.ERROR_NICK_IN_USE);
mucOptions.setError(MucOptions.Error.NICK_IN_USE);
}
} else if (error != null && error.hasChild("not-authorized")) {
mucOptions.setError(MucOptions.ERROR_PASSWORD_REQUIRED);
mucOptions.setError(MucOptions.Error.PASSWORD_REQUIRED);
} else if (error != null && error.hasChild("forbidden")) {
mucOptions.setError(MucOptions.ERROR_BANNED);
mucOptions.setError(MucOptions.Error.BANNED);
} else if (error != null && error.hasChild("registration-required")) {
mucOptions.setError(MucOptions.ERROR_MEMBERS_ONLY);
mucOptions.setError(MucOptions.Error.MEMBERS_ONLY);
}
}
}
@ -161,29 +177,58 @@ public class PresenceParser extends AbstractParser implements
public void parseContactPresence(final PresencePacket packet, final Account account) {
final PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator();
final Jid from = packet.getFrom();
if (from == null) {
if (from == null || from.equals(account.getJid())) {
return;
}
final String type = packet.getAttribute("type");
final Contact contact = account.getRoster().getContact(from);
if (type == null) {
String presence = from.isBareJid() ? "" : from.getResourcepart();
final String resource = from.isBareJid() ? "" : from.getResourcepart();
contact.setPresenceName(packet.findChildContent("nick", "http://jabber.org/protocol/nick"));
Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
if (avatar != null && !contact.isSelf()) {
if (avatar != null && (!contact.isSelf() || account.getAvatar() == null)) {
avatar.owner = from.toBareJid();
if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) {
if (contact.setAvatar(avatar)) {
if (avatar.owner.equals(account.getJid().toBareJid())) {
account.setAvatar(avatar.getFilename());
mXmppConnectionService.databaseBackend.updateAccount(account);
mXmppConnectionService.getAvatarService().clear(account);
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateAccountUi();
} else if (contact.setAvatar(avatar)) {
mXmppConnectionService.getAvatarService().clear(contact);
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateRosterUi();
}
} else {
} else if (mXmppConnectionService.isDataSaverDisabled()){
mXmppConnectionService.fetchAvatar(account, avatar);
}
}
int sizeBefore = contact.getPresences().size();
contact.updatePresence(presence, Presences.parseShow(packet.findChild("show")));
final String show = packet.findChildContent("show");
final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps");
final String message = packet.findChildContent("status");
final Presence presence = Presence.parse(show, caps, message);
contact.updatePresence(resource, presence);
if (presence.hasCaps()) {
mXmppConnectionService.fetchCaps(account, from, presence);
}
final Element idle = packet.findChild("idle","urn:xmpp:idle:1");
if (idle != null) {
contact.flagInactive();
String since = idle.getAttribute("since");
try {
contact.setLastseen(AbstractParser.parseTimestamp(since));
} catch (NullPointerException | ParseException e) {
contact.setLastseen(System.currentTimeMillis());
}
} else {
contact.flagActive();
contact.setLastseen(AbstractParser.parseTimestamp(packet));
}
PgpEngine pgp = mXmppConnectionService.getPgpEngine();
Element x = packet.findChild("x", "jabber:x:signed");
if (pgp != null && x != null) {
@ -192,7 +237,6 @@ public class PresenceParser extends AbstractParser implements
contact.setPgpKeyId(pgp.fetchKeyId(account, msg, x.getContent()));
}
boolean online = sizeBefore < contact.getPresences().size();
updateLastseen(packet, account, false);
mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online);
} else if (type.equals("unavailable")) {
if (from.isBareJid()) {

View file

@ -7,10 +7,12 @@ import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.os.Environment;
import android.util.Base64;
import android.util.Log;
import android.util.Pair;
import org.json.JSONObject;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
@ -20,27 +22,34 @@ import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import org.json.JSONException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.PresenceTemplate;
import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@ -49,7 +58,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 22;
private static final int DATABASE_VERSION = 34;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -63,6 +72,22 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ Contact.JID + ") ON CONFLICT REPLACE);";
private static String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table "
+ ServiceDiscoveryResult.TABLENAME + "("
+ ServiceDiscoveryResult.HASH + " TEXT, "
+ ServiceDiscoveryResult.VER + " TEXT, "
+ ServiceDiscoveryResult.RESULT + " TEXT, "
+ "UNIQUE(" + ServiceDiscoveryResult.HASH + ", "
+ ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);";
private static String CREATE_PRESENCE_TEMPLATES_STATEMENT = "CREATE TABLE "
+ PresenceTemplate.TABELNAME + "("
+ PresenceTemplate.UUID + " TEXT, "
+ PresenceTemplate.LAST_USED + " NUMBER,"
+ PresenceTemplate.MESSAGE + " TEXT,"
+ PresenceTemplate.STATUS + " TEXT,"
+ "UNIQUE("+PresenceTemplate.MESSAGE + "," +PresenceTemplate.STATUS+") ON CONFLICT REPLACE);";
private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+ SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+ SQLiteAxolotlStore.ACCOUNT + " TEXT, "
@ -108,7 +133,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ SQLiteAxolotlStore.OWN + " INTEGER, "
+ SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ SQLiteAxolotlStore.CERTIFICATE + " BLOB, "
+ SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ SQLiteAxolotlStore.TRUST + " TEXT, "
+ SQLiteAxolotlStore.ACTIVE + " NUMBER, "
+ SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER,"
+ SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
@ -118,6 +145,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") ON CONFLICT IGNORE"
+ ");";
private static String START_TIMES_TABLE = "start_times";
private static String CREATE_START_TIMES_TABLE = "create table "+START_TIMES_TABLE+" (timestamp NUMBER);";
private static String CREATE_MESSAGE_TIME_INDEX = "create INDEX message_time_index ON "+Message.TABLENAME+"("+Message.TIME_SENT+")";
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@ -125,13 +158,19 @@ public class DatabaseBackend extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL("PRAGMA foreign_keys=ON;");
db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID
+ " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT,"
+ Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT,"
db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID+ " TEXT PRIMARY KEY,"
+ Account.USERNAME + " TEXT,"
+ Account.SERVER + " TEXT,"
+ Account.PASSWORD + " TEXT,"
+ Account.DISPLAY_NAME + " TEXT, "
+ Account.ROSTERVERSION + " TEXT," + Account.OPTIONS
+ " NUMBER, " + Account.AVATAR + " TEXT, " + Account.KEYS
+ " TEXT, " + Account.HOSTNAME + " TEXT, " + Account.PORT + " NUMBER DEFAULT 5222)");
+ Account.STATUS + " TEXT,"
+ Account.STATUS_MESSAGE + " TEXT,"
+ Account.ROSTERVERSION + " TEXT,"
+ Account.OPTIONS + " NUMBER, "
+ Account.AVATAR + " TEXT, "
+ Account.KEYS + " TEXT, "
+ Account.HOSTNAME + " TEXT, "
+ Account.PORT + " NUMBER DEFAULT 5222)");
db.execSQL("create table " + Conversation.TABLENAME + " ("
+ Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
+ " TEXT, " + Conversation.CONTACT + " TEXT, "
@ -151,17 +190,23 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.SERVER_MSG_ID + " TEXT, "
+ Message.FINGERPRINT + " TEXT, "
+ Message.CARBON + " INTEGER, "
+ Message.EDITED + " TEXT, "
+ Message.READ + " NUMBER DEFAULT 1, "
+ Message.OOB + " INTEGER, "
+ Message.ERROR_MESSAGE + " TEXT,"
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
+ ") ON DELETE CASCADE);");
db.execSQL(CREATE_MESSAGE_TIME_INDEX);
db.execSQL(CREATE_CONTATCS_STATEMENT);
db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
db.execSQL(CREATE_START_TIMES_TABLE);
}
@Override
@ -221,91 +266,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("update " + Account.TABLENAME + " set " + Account.ROSTERVERSION + " = NULL");
}
if (oldVersion < 14 && newVersion >= 14) {
// migrate db to new, canonicalized JID domainpart representation
// Conversation table
Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]);
while (cursor.moveToNext()) {
String newJid;
try {
newJid = Jid.fromString(
cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
).toString();
} catch (InvalidJidException ignored) {
Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID "
+ cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
+ ": " + ignored + ". Skipping...");
continue;
}
String updateArgs[] = {
newJid,
cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
};
db.execSQL("update " + Conversation.TABLENAME
+ " set " + Conversation.CONTACTJID + " = ? "
+ " where " + Conversation.UUID + " = ?", updateArgs);
}
cursor.close();
// Contact table
cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]);
while (cursor.moveToNext()) {
String newJid;
try {
newJid = Jid.fromString(
cursor.getString(cursor.getColumnIndex(Contact.JID))
).toString();
} catch (InvalidJidException ignored) {
Log.e(Config.LOGTAG, "Failed to migrate Contact JID "
+ cursor.getString(cursor.getColumnIndex(Contact.JID))
+ ": " + ignored + ". Skipping...");
continue;
}
String updateArgs[] = {
newJid,
cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
cursor.getString(cursor.getColumnIndex(Contact.JID)),
};
db.execSQL("update " + Contact.TABLENAME
+ " set " + Contact.JID + " = ? "
+ " where " + Contact.ACCOUNT + " = ? "
+ " AND " + Contact.JID + " = ?", updateArgs);
}
cursor.close();
// Account table
cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]);
while (cursor.moveToNext()) {
String newServer;
try {
newServer = Jid.fromParts(
cursor.getString(cursor.getColumnIndex(Account.USERNAME)),
cursor.getString(cursor.getColumnIndex(Account.SERVER)),
"mobile"
).getDomainpart();
} catch (InvalidJidException ignored) {
Log.e(Config.LOGTAG, "Failed to migrate Account SERVER "
+ cursor.getString(cursor.getColumnIndex(Account.SERVER))
+ ": " + ignored + ". Skipping...");
continue;
}
String updateArgs[] = {
newServer,
cursor.getString(cursor.getColumnIndex(Account.UUID)),
};
db.execSQL("update " + Account.TABLENAME
+ " set " + Account.SERVER + " = ? "
+ " where " + Account.UUID + " = ?", updateArgs);
}
cursor.close();
canonicalizeJids(db);
}
if (oldVersion < 15 && newVersion >= 15) {
recreateAxolotlDb(db);
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.FINGERPRINT + " TEXT");
} else if (oldVersion < 22 && newVersion >= 22) {
db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE);
}
if (oldVersion < 16 && newVersion >= 16) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
@ -318,6 +286,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.HOSTNAME + " TEXT");
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.PORT + " NUMBER DEFAULT 5222");
}
if (oldVersion < 26 && newVersion >= 26) {
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS + " TEXT");
db.execSQL("ALTER TABLE " + Account.TABLENAME + " ADD COLUMN " + Account.STATUS_MESSAGE + " TEXT");
}
/* Any migrations that alter the Account table need to happen BEFORE this migration, as it
* depends on account de-serialization.
*/
@ -329,11 +301,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
continue;
}
int ownDeviceId = Integer.valueOf(ownDeviceIdString);
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), ownDeviceId);
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toPreppedString(), ownDeviceId);
deleteSession(db, account, ownAddress);
IdentityKeyPair identityKeyPair = loadOwnIdentityKeyPair(db, account);
if (identityKeyPair != null) {
setIdentityKeyTrust(db, account, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.TRUSTED);
String[] selectionArgs = {
account.getUuid(),
identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", "")
};
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.TRUSTED, 2);
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs);
} else {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not load own identity key pair");
}
@ -352,9 +333,195 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
if (oldVersion < 22 && newVersion >= 22) {
db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE);
if (oldVersion < 23 && newVersion >= 23) {
db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT);
}
if (oldVersion < 24 && newVersion >= 24) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT");
}
if (oldVersion < 25 && newVersion >= 25) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.OOB + " INTEGER");
}
if (oldVersion < 26 && newVersion >= 26) {
db.execSQL(CREATE_PRESENCE_TEMPLATES_STATEMENT);
}
if (oldVersion < 27 && newVersion >= 27) {
db.execSQL("DELETE FROM "+ServiceDiscoveryResult.TABLENAME);
}
if (oldVersion < 28 && newVersion >= 28) {
canonicalizeJids(db);
}
if (oldVersion < 29 && newVersion >= 29) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.ERROR_MESSAGE + " TEXT");
}
if (oldVersion < 30 && newVersion >= 30) {
db.execSQL(CREATE_START_TIMES_TABLE);
}
if (oldVersion < 31 && newVersion >= 31) {
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.TRUST + " TEXT");
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.ACTIVE + " NUMBER");
HashMap<Integer,ContentValues> migration = new HashMap<>();
migration.put(0,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED,true));
migration.put(1,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, true));
migration.put(2,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, true));
migration.put(3,createFingerprintStatusContentValues(FingerprintStatus.Trust.COMPROMISED, false));
migration.put(4,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
migration.put(5,createFingerprintStatusContentValues(FingerprintStatus.Trust.TRUSTED, false));
migration.put(6,createFingerprintStatusContentValues(FingerprintStatus.Trust.UNTRUSTED, false));
migration.put(7,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, true));
migration.put(8,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED_X509, false));
for(Map.Entry<Integer,ContentValues> entry : migration.entrySet()) {
String whereClause = SQLiteAxolotlStore.TRUSTED+"=?";
String[] where = {String.valueOf(entry.getKey())};
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,entry.getValue(),whereClause,where);
}
}
if (oldVersion < 32 && newVersion >= 32) {
db.execSQL("ALTER TABLE "+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN "+SQLiteAxolotlStore.LAST_ACTIVATION + " NUMBER");
ContentValues defaults = new ContentValues();
defaults.put(SQLiteAxolotlStore.LAST_ACTIVATION,System.currentTimeMillis());
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,defaults,null,null);
}
if (oldVersion < 33 && newVersion >= 33) {
String whereClause = SQLiteAxolotlStore.OWN+"=1";
db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,createFingerprintStatusContentValues(FingerprintStatus.Trust.VERIFIED,true),whereClause,null);
}
if (oldVersion < 34 && newVersion >= 34) {
db.execSQL(CREATE_MESSAGE_TIME_INDEX);
final File oldPicturesDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)+"/Conversations/");
final File oldFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/");
final File newFilesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Files/");
final File newVideosDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Videos/");
if (oldPicturesDirectory.exists() && oldPicturesDirectory.isDirectory()) {
final File newPicturesDirectory = new File(Environment.getExternalStorageDirectory() + "/Conversations/Media/Conversations Images/");
newPicturesDirectory.getParentFile().mkdirs();
if (oldPicturesDirectory.renameTo(newPicturesDirectory)) {
Log.d(Config.LOGTAG,"moved "+oldPicturesDirectory.getAbsolutePath()+" to "+newPicturesDirectory.getAbsolutePath());
}
}
if (oldFilesDirectory.exists() && oldFilesDirectory.isDirectory()) {
newFilesDirectory.mkdirs();
newVideosDirectory.mkdirs();
for(File file : oldFilesDirectory.listFiles()) {
if (file.getName().equals(".nomedia")) {
if (file.delete()) {
Log.d(Config.LOGTAG,"deleted nomedia file in "+oldFilesDirectory.getAbsolutePath());
}
} else if (file.isFile()) {
final String name = file.getName();
boolean isVideo = false;
int start = name.lastIndexOf('.') + 1;
if (start < name.length()) {
String mime= MimeUtils.guessMimeTypeFromExtension(name.substring(start));
isVideo = mime != null && mime.startsWith("video/");
}
File dst = new File((isVideo ? newVideosDirectory : newFilesDirectory).getAbsolutePath()+"/"+file.getName());
if (file.renameTo(dst)) {
Log.d(Config.LOGTAG, "moved " + file + " to " + dst);
}
}
}
}
}
}
private static ContentValues createFingerprintStatusContentValues(FingerprintStatus.Trust trust, boolean active) {
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.TRUST,trust.toString());
values.put(SQLiteAxolotlStore.ACTIVE,active ? 1 : 0);
return values;
}
private void canonicalizeJids(SQLiteDatabase db) {
// migrate db to new, canonicalized JID domainpart representation
// Conversation table
Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME, new String[0]);
while (cursor.moveToNext()) {
String newJid;
try {
newJid = Jid.fromString(
cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
).toPreppedString();
} catch (InvalidJidException ignored) {
Log.e(Config.LOGTAG, "Failed to migrate Conversation CONTACTJID "
+ cursor.getString(cursor.getColumnIndex(Conversation.CONTACTJID))
+ ": " + ignored + ". Skipping...");
continue;
}
String updateArgs[] = {
newJid,
cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
};
db.execSQL("update " + Conversation.TABLENAME
+ " set " + Conversation.CONTACTJID + " = ? "
+ " where " + Conversation.UUID + " = ?", updateArgs);
}
cursor.close();
// Contact table
cursor = db.rawQuery("select * from " + Contact.TABLENAME, new String[0]);
while (cursor.moveToNext()) {
String newJid;
try {
newJid = Jid.fromString(
cursor.getString(cursor.getColumnIndex(Contact.JID))
).toPreppedString();
} catch (InvalidJidException ignored) {
Log.e(Config.LOGTAG, "Failed to migrate Contact JID "
+ cursor.getString(cursor.getColumnIndex(Contact.JID))
+ ": " + ignored + ". Skipping...");
continue;
}
String updateArgs[] = {
newJid,
cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
cursor.getString(cursor.getColumnIndex(Contact.JID)),
};
db.execSQL("update " + Contact.TABLENAME
+ " set " + Contact.JID + " = ? "
+ " where " + Contact.ACCOUNT + " = ? "
+ " AND " + Contact.JID + " = ?", updateArgs);
}
cursor.close();
// Account table
cursor = db.rawQuery("select * from " + Account.TABLENAME, new String[0]);
while (cursor.moveToNext()) {
String newServer;
try {
newServer = Jid.fromParts(
cursor.getString(cursor.getColumnIndex(Account.USERNAME)),
cursor.getString(cursor.getColumnIndex(Account.SERVER)),
"mobile"
).getDomainpart();
} catch (InvalidJidException ignored) {
Log.e(Config.LOGTAG, "Failed to migrate Account SERVER "
+ cursor.getString(cursor.getColumnIndex(Account.SERVER))
+ ": " + ignored + ". Skipping...");
continue;
}
String updateArgs[] = {
newServer,
cursor.getString(cursor.getColumnIndex(Account.UUID)),
};
db.execSQL("update " + Account.TABLENAME
+ " set " + Account.SERVER + " = ? "
+ " where " + Account.UUID + " = ?", updateArgs);
}
cursor.close();
}
public static synchronized DatabaseBackend getInstance(Context context) {
@ -379,6 +546,56 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.insert(Account.TABLENAME, null, account.getContentValues());
}
public void insertDiscoveryResult(ServiceDiscoveryResult result) {
SQLiteDatabase db = this.getWritableDatabase();
db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues());
}
public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) {
SQLiteDatabase db = this.getReadableDatabase();
String[] selectionArgs = {hash, ver};
Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null,
ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?",
selectionArgs, null, null, null);
if (cursor.getCount() == 0) {
cursor.close();
return null;
}
cursor.moveToFirst();
ServiceDiscoveryResult result = null;
try {
result = new ServiceDiscoveryResult(cursor);
} catch (JSONException e) { /* result is still null */ }
cursor.close();
return result;
}
public void insertPresenceTemplate(PresenceTemplate template) {
SQLiteDatabase db = this.getWritableDatabase();
db.insert(PresenceTemplate.TABELNAME, null, template.getContentValues());
}
public List<PresenceTemplate> getPresenceTemplates() {
ArrayList<PresenceTemplate> templates = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.query(PresenceTemplate.TABELNAME,null,null,null,null,null,PresenceTemplate.LAST_USED+" desc");
while (cursor.moveToNext()) {
templates.add(PresenceTemplate.fromCursor(cursor));
}
cursor.close();
return templates;
}
public void deletePresenceTemplate(PresenceTemplate template) {
Log.d(Config.LOGTAG,"deleting presence template with uuid "+template.getUuid());
SQLiteDatabase db = this.getWritableDatabase();
String where = PresenceTemplate.UUID+"=?";
String[] whereArgs = {template.getUuid()};
db.delete(PresenceTemplate.TABELNAME,where,whereArgs);
}
public CopyOnWriteArrayList<Conversation> getConversations(int status) {
CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
@ -467,14 +684,16 @@ public class DatabaseBackend extends SQLiteOpenHelper {
public Conversation findConversation(final Account account, final Jid contactJid) {
SQLiteDatabase db = this.getReadableDatabase();
String[] selectionArgs = {account.getUuid(),
contactJid.toBareJid().toString() + "/%",
contactJid.toBareJid().toString()
contactJid.toBareJid().toPreppedString() + "/%",
contactJid.toBareJid().toPreppedString()
};
Cursor cursor = db.query(Conversation.TABLENAME, null,
Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
+ " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
if (cursor.getCount() == 0)
if (cursor.getCount() == 0) {
cursor.close();
return null;
}
cursor.moveToFirst();
Conversation conversation = Conversation.fromCursor(cursor);
cursor.close();
@ -504,17 +723,18 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return list;
}
public void updateAccount(Account account) {
public boolean updateAccount(Account account) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid()};
db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
+ "=?", args);
final int rows = db.update(Account.TABLENAME, account.getContentValues(), Account.UUID + "=?", args);
return rows == 1;
}
public void deleteAccount(Account account) {
public boolean deleteAccount(Account account) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid()};
db.delete(Account.TABLENAME, Account.UUID + "=?", args);
final int rows = db.delete(Account.TABLENAME, Account.UUID + "=?", args);
return rows == 1;
}
public boolean hasEnabledAccounts() {
@ -524,12 +744,15 @@ public class DatabaseBackend extends SQLiteOpenHelper {
try {
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
return (count > 0);
} catch (SQLiteCantOpenDatabaseException e) {
return true; // better safe than sorry
} catch (RuntimeException e) {
return true; // better safe than sorry
} finally {
if (cursor != null) {
cursor.close();
}
}
}
@ -547,6 +770,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ "=?", args);
}
public void updateMessage(Message message, String uuid) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {uuid};
db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
+ "=?", args);
}
public void readRoster(Roster roster) {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor;
@ -567,7 +797,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.insert(Contact.TABLENAME, null, contact.getContentValues());
} else {
String where = Contact.ACCOUNT + "=? AND " + Contact.JID + "=?";
String[] whereArgs = {account.getUuid(), contact.getJid().toString()};
String[] whereArgs = {account.getUuid(), contact.getJid().toPreppedString()};
db.delete(Contact.TABLENAME, where, whereArgs);
}
}
@ -583,12 +813,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
db.delete(Message.TABLENAME, Message.CONVERSATION + "=?", args);
}
public boolean expireOldMessages(long timestamp) {
String where = Message.TIME_SENT+"<?";
String[] whereArgs = {String.valueOf(timestamp)};
SQLiteDatabase db = this.getReadableDatabase();
return db.delete(Message.TABLENAME,where,whereArgs) > 0;
}
public Pair<Long, String> getLastMessageReceived(Account account) {
Cursor cursor = null;
try {
SQLiteDatabase db = this.getReadableDatabase();
String sql = "select messages.timeSent,messages.serverMsgId from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and (messages.status=0 or messages.carbon=1 or messages.serverMsgId not null) order by messages.timesent desc limit 1";
String[] args = {account.getUuid()};
Cursor cursor = db.rawQuery(sql, args);
cursor = db.rawQuery(sql, args);
if (cursor.getCount() == 0) {
return null;
} else {
@ -597,24 +835,58 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
} catch (Exception e) {
return null;
} finally {
if (cursor != null) {
cursor.close();
}
}
}
public long getLastTimeFingerprintUsed(Account account, String fingerprint) {
String SQL = "select messages.timeSent from accounts join conversations on accounts.uuid=conversations.accountUuid join messages on conversations.uuid=messages.conversationUuid where accounts.uuid=? and messages.axolotl_fingerprint=? order by messages.timesent desc limit 1";
String[] args = {account.getUuid(), fingerprint};
Cursor cursor = getReadableDatabase().rawQuery(SQL,args);
long time;
if (cursor.moveToFirst()) {
time = cursor.getLong(0);
} else {
time = 0;
}
cursor.close();
return time;
}
public Pair<Long,String> getLastClearDate(Account account) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {Conversation.ATTRIBUTES};
String selection = Conversation.ACCOUNT + "=?";
String[] args = {account.getUuid()};
Cursor cursor = db.query(Conversation.TABLENAME,columns,selection,args,null,null,null);
long maxClearDate = 0;
while (cursor.moveToNext()) {
try {
final JSONObject jsonObject = new JSONObject(cursor.getString(0));
maxClearDate = Math.max(maxClearDate, jsonObject.getLong(Conversation.ATTRIBUTE_LAST_CLEAR_HISTORY));
} catch (Exception e) {
//ignored
}
}
cursor.close();
return new Pair<>(maxClearDate,null);
}
private Cursor getCursorForSession(Account account, AxolotlAddress contact) {
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = null;
String[] selectionArgs = {account.getUuid(),
contact.getName(),
Integer.toString(contact.getDeviceId())};
Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
columns,
return db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
null,
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.NAME + " = ? AND "
+ SQLiteAxolotlStore.DEVICE_ID + " = ? ",
selectionArgs,
null, null, null);
return cursor;
}
public SessionRecord loadSession(Account account, AxolotlAddress contact) {
@ -848,7 +1120,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
private Cursor getIdentityKeyCursor(SQLiteDatabase db, Account account, String name, Boolean own, String fingerprint) {
String[] columns = {SQLiteAxolotlStore.TRUSTED,
String[] columns = {SQLiteAxolotlStore.TRUST,
SQLiteAxolotlStore.ACTIVE,
SQLiteAxolotlStore.LAST_ACTIVATION,
SQLiteAxolotlStore.KEY};
ArrayList<String> selectionArgs = new ArrayList<>(4);
selectionArgs.add(account.getUuid());
@ -880,7 +1154,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
private IdentityKeyPair loadOwnIdentityKeyPair(SQLiteDatabase db, Account account) {
String name = account.getJid().toBareJid().toString();
String name = account.getJid().toBareJid().toPreppedString();
IdentityKeyPair identityKeyPair = null;
Cursor cursor = getIdentityKeyCursor(db, account, name, true);
if (cursor.getCount() != 0) {
@ -900,18 +1174,21 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return loadIdentityKeys(account, name, null);
}
public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
public Set<IdentityKey> loadIdentityKeys(Account account, String name, FingerprintStatus status) {
Set<IdentityKey> identityKeys = new HashSet<>();
Cursor cursor = getIdentityKeyCursor(account, name, false);
while (cursor.moveToNext()) {
if (trust != null &&
cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED))
!= trust.getCode()) {
if (status != null && !FingerprintStatus.fromCursor(cursor).equals(status)) {
continue;
}
try {
identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT), 0));
String key = cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY));
if (key != null) {
identityKeys.add(new IdentityKey(Base64.decode(key, Base64.DEFAULT), 0));
} else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Missing key (possibly preverified) in database for account" + account.getJid().toBareJid() + ", address: " + name);
}
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
}
@ -926,22 +1203,20 @@ public class DatabaseBackend extends SQLiteOpenHelper {
String[] args = {
account.getUuid(),
name,
String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode()),
String.valueOf(XmppAxolotlSession.Trust.TRUSTED_X509.getCode())
FingerprintStatus.Trust.TRUSTED.toString(),
FingerprintStatus.Trust.VERIFIED.toString(),
FingerprintStatus.Trust.VERIFIED_X509.toString()
};
return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ?"
+ " AND " + SQLiteAxolotlStore.NAME + " = ?"
+ " AND (" + SQLiteAxolotlStore.TRUSTED + " = ? OR " + SQLiteAxolotlStore.TRUSTED + " = ?)",
+ " AND (" + SQLiteAxolotlStore.TRUST + " = ? OR " + SQLiteAxolotlStore.TRUST + " = ? OR " +SQLiteAxolotlStore.TRUST +" = ?)"
+ " AND " +SQLiteAxolotlStore.ACTIVE + " > 0",
args
);
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
storeIdentityKey(account, name, own, fingerprint, base64Serialized, XmppAxolotlSession.Trust.UNDECIDED);
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, XmppAxolotlSession.Trust trusted) {
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, FingerprintStatus status) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
@ -949,35 +1224,50 @@ public class DatabaseBackend extends SQLiteOpenHelper {
values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.put(SQLiteAxolotlStore.KEY, base64Serialized);
values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode());
values.putAll(status.toContentValues());
String where = SQLiteAxolotlStore.ACCOUNT+"=? AND "+SQLiteAxolotlStore.NAME+"=? AND "+SQLiteAxolotlStore.FINGERPRINT+" =?";
String[] whereArgs = {account.getUuid(),name,fingerprint};
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME,values,where,whereArgs);
if (rows == 0) {
db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
}
}
public void storePreVerification(Account account, String name, String fingerprint, FingerprintStatus status) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
values.put(SQLiteAxolotlStore.NAME, name);
values.put(SQLiteAxolotlStore.OWN, 0);
values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.putAll(status.toContentValues());
db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
}
public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
public FingerprintStatus getFingerprintStatus(Account account, String fingerprint) {
Cursor cursor = getIdentityKeyCursor(account, fingerprint);
XmppAxolotlSession.Trust trust = null;
final FingerprintStatus status;
if (cursor.getCount() > 0) {
cursor.moveToFirst();
int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED));
trust = XmppAxolotlSession.Trust.fromCode(trustValue);
status = FingerprintStatus.fromCursor(cursor);
} else {
status = null;
}
cursor.close();
return trust;
return status;
}
public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
public boolean setIdentityKeyTrust(Account account, String fingerprint, FingerprintStatus fingerprintStatus) {
SQLiteDatabase db = this.getWritableDatabase();
return setIdentityKeyTrust(db, account, fingerprint, trust);
return setIdentityKeyTrust(db, account, fingerprint, fingerprintStatus);
}
private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
private boolean setIdentityKeyTrust(SQLiteDatabase db, Account account, String fingerprint, FingerprintStatus status) {
String[] selectionArgs = {
account.getUuid(),
fingerprint
};
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode());
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, status.toContentValues(),
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs);
@ -1017,6 +1307,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
} else {
cursor.moveToFirst();
byte[] certificate = cursor.getBlob(cursor.getColumnIndex(SQLiteAxolotlStore.CERTIFICATE));
cursor.close();
if (certificate == null || certificate.length == 0) {
return null;
}
@ -1030,17 +1321,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
}
public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
public void storeIdentityKey(Account account, String name, IdentityKey identityKey, FingerprintStatus status) {
storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT), status);
}
public void storeOwnIdentityKeyPair(Account account, IdentityKeyPair identityKeyPair) {
storeIdentityKey(account, account.getJid().toBareJid().toString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED);
storeIdentityKey(account, account.getJid().toBareJid().toPreppedString(), true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), FingerprintStatus.createActiveVerified(false));
}
public void recreateAxolotlDb() {
recreateAxolotlDb(getWritableDatabase());
}
public void recreateAxolotlDb(SQLiteDatabase db) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + ">>> (RE)CREATING AXOLOTL DATABASE <<<");
@ -1074,4 +1362,35 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
}
public boolean startTimeCountExceedsThreshold() {
SQLiteDatabase db = this.getWritableDatabase();
long cleanBeforeTimestamp = System.currentTimeMillis() - Config.FREQUENT_RESTARTS_DETECTION_WINDOW;
db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp < "+cleanBeforeTimestamp);
ContentValues values = new ContentValues();
values.put("timestamp",System.currentTimeMillis());
db.insert(START_TIMES_TABLE,null,values);
String[] columns = new String[]{"count(timestamp)"};
Cursor cursor = db.query(START_TIMES_TABLE,columns,null,null,null,null,null);
int count;
if (cursor.moveToFirst()) {
count = cursor.getInt(0);
} else {
count = 0;
}
cursor.close();
Log.d(Config.LOGTAG,"start time counter reached "+count);
return count >= Config.FREQUENT_RESTARTS_THRESHOLD;
}
public void clearStartTimeCounter(boolean justOne) {
SQLiteDatabase db = this.getWritableDatabase();
if (justOne) {
db.execSQL("delete from "+START_TIMES_TABLE+" where timestamp in (select timestamp from "+START_TIMES_TABLE+" order by timestamp desc limit 1)");
Log.d(Config.LOGTAG,"do not count start up after being swiped away");
} else {
Log.d(Config.LOGTAG,"resetting start time counter");
db.execSQL("delete from " + START_TIMES_TABLE);
}
}
}

View file

@ -1,20 +1,37 @@
package eu.siacs.conversations.persistance;
import android.annotation.TargetApi;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.support.v4.content.FileProvider;
import android.system.Os;
import android.system.StructStat;
import android.util.Base64;
import android.util.Base64OutputStream;
import android.util.Log;
import android.util.LruCache;
import android.webkit.MimeTypeMap;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
@ -26,23 +43,26 @@ import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.utils.FileWriterException;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend {
private final SimpleDateFormat imageDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
private static final SimpleDateFormat IMAGE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
private static final String FILE_PROVIDER = ".files";
private XmppConnectionService mXmppConnectionService;
@ -50,6 +70,38 @@ public class FileBackend {
this.mXmppConnectionService = service;
}
private void createNoMedia() {
final File nomedia = new File(getConversationsDirectory("Files")+".nomedia");
if (!nomedia.exists()) {
try {
nomedia.createNewFile();
} catch (Exception e) {
Log.d(Config.LOGTAG, "could not create nomedia file");
}
}
}
public void updateMediaScanner(File file) {
String path = file.getAbsolutePath();
if (!path.startsWith(getConversationsDirectory("Files"))) {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
mXmppConnectionService.sendBroadcast(intent);
} else {
createNoMedia();
}
}
public boolean deleteFile(Message message) {
File file = getFile(message);
if (file.delete()) {
updateMediaScanner(file);
return true;
} else {
return false;
}
}
public DownloadableFile getFile(Message message) {
return getFile(message, true);
}
@ -67,27 +119,54 @@ public class FileBackend {
file = new DownloadableFile(path);
} else {
String mime = message.getMimeType();
if (mime != null && mime.startsWith("image")) {
file = new DownloadableFile(getConversationsImageDirectory() + path);
if (mime != null && mime.startsWith("image/")) {
file = new DownloadableFile(getConversationsDirectory("Images") + path);
} else if (mime != null && mime.startsWith("video/")) {
file = new DownloadableFile(getConversationsDirectory("Videos") + path);
} else {
file = new DownloadableFile(getConversationsFileDirectory() + path);
file = new DownloadableFile(getConversationsDirectory("Files") + path);
}
}
if (encrypted) {
return new DownloadableFile(getConversationsFileDirectory() + file.getName() + ".pgp");
return new DownloadableFile(getConversationsDirectory("Files") + file.getName() + ".pgp");
} else {
return file;
}
}
public static String getConversationsFileDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
private static long getFileSize(Context context, Uri uri) {
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
return cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE));
} else {
return -1;
}
}
public static String getConversationsImageDirectory() {
return Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).getAbsolutePath()
+ "/Conversations/";
public static boolean allFilesUnderSize(Context context, List<Uri> uris, long max) {
if (max <= 0) {
Log.d(Config.LOGTAG,"server did not report max file size for http upload");
return true; //exception to be compatible with HTTP Upload < v0.2
}
for(Uri uri : uris) {
if (FileBackend.getFileSize(context, uri) > max) {
Log.d(Config.LOGTAG,"not all files are under "+max+" bytes. suggesting falling back to jingle");
return false;
}
}
return true;
}
public String getConversationsDirectory(final String type) {
if (Config.ONLY_INTERNAL_STORAGE) {
return mXmppConnectionService.getFilesDir().getAbsolutePath()+"/"+type+"/";
} else {
return Environment.getExternalStorageDirectory() +"/Conversations/Media/Conversations "+type+"/";
}
}
public static String getConversationsLogsDirectory() {
return Environment.getExternalStorageDirectory().getAbsolutePath()+"/Conversations/";
}
public Bitmap resize(Bitmap originalBitmap, int size) {
@ -156,6 +235,7 @@ public class FileBackend {
}
public void copyFileToPrivateStorage(File file, Uri uri) throws FileCopyException {
Log.d(Config.LOGTAG,"copy file ("+uri.toString()+") to private storage "+file.getAbsolutePath());
file.getParentFile().mkdirs();
OutputStream os = null;
InputStream is = null;
@ -166,11 +246,21 @@ public class FileBackend {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
try {
os.write(buffer, 0, length);
} catch (IOException e) {
throw new FileWriterException();
}
}
try {
os.flush();
} catch (IOException e) {
throw new FileWriterException();
}
os.flush();
} catch(FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found);
} catch(FileWriterException e) {
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
} catch (IOException e) {
e.printStackTrace();
throw new FileCopyException(R.string.error_io_exception);
@ -178,24 +268,50 @@ public class FileBackend {
close(os);
close(is);
}
Log.d(Config.LOGTAG, "output file name " + file.getAbsolutePath());
}
public void copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage");
String mime = mXmppConnectionService.getContentResolver().getType(uri);
String mime = MimeUtils.guessMimeTypeFromUri(mXmppConnectionService, uri);
Log.d(Config.LOGTAG, "copy " + uri.toString() + " to private storage (mime="+mime+")");
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
if (extension == null) {
extension = getExtensionFromUri(uri);
}
message.setRelativeFilePath(message.getUuid() + "." + extension);
copyFileToPrivateStorage(mXmppConnectionService.getFileBackend().getFile(message), uri);
}
private String getExtensionFromUri(Uri uri) {
String[] projection = {MediaStore.MediaColumns.DATA};
String filename = null;
Cursor cursor = mXmppConnectionService.getContentResolver().query(uri, projection, null, null, null);
if (cursor != null) {
try {
if (cursor.moveToFirst()) {
filename = cursor.getString(0);
}
} catch (Exception e) {
filename = null;
} finally {
cursor.close();
}
}
int pos = filename == null ? -1 : filename.lastIndexOf('.');
return pos > 0 ? filename.substring(pos+1) : null;
}
private void copyImageToPrivateStorage(File file, Uri image, int sampleSize) throws FileCopyException {
file.getParentFile().mkdirs();
InputStream is = null;
OutputStream os = null;
try {
file.createNewFile();
if (!file.exists() && !file.createNewFile()) {
throw new FileCopyException(R.string.error_unable_to_create_temporary_file);
}
is = mXmppConnectionService.getContentResolver().openInputStream(image);
if (is == null) {
throw new FileCopyException(R.string.error_not_an_image_file);
}
Bitmap originalBitmap;
BitmapFactory.Options options = new BitmapFactory.Options();
int inSampleSize = (int) Math.pow(2, sampleSize);
@ -222,7 +338,6 @@ public class FileBackend {
quality -= 5;
}
scaledBitmap.recycle();
return;
} catch (FileNotFoundException e) {
throw new FileCopyException(R.string.error_file_not_found);
} catch (IOException e) {
@ -237,8 +352,6 @@ public class FileBackend {
} else {
throw new FileCopyException(R.string.error_out_of_memory);
}
} catch (NullPointerException e) {
throw new FileCopyException(R.string.error_io_exception);
} finally {
close(os);
close(is);
@ -246,6 +359,7 @@ public class FileBackend {
}
public void copyImageToPrivateStorage(File file, Uri image) throws FileCopyException {
Log.d(Config.LOGTAG,"copy image ("+image.toString()+") to private storage "+file.getAbsolutePath());
copyImageToPrivateStorage(file, image, 0);
}
@ -281,35 +395,121 @@ public class FileBackend {
}
}
public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
throws FileNotFoundException {
Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(message.getUuid());
public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException {
final String uuid = message.getUuid();
final LruCache<String,Bitmap> cache = mXmppConnectionService.getBitmapCache();
Bitmap thumbnail = cache.get(uuid);
if ((thumbnail == null) && (!cacheOnly)) {
File file = getFile(message);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(file, size);
Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),options);
if (fullsize == null) {
throw new FileNotFoundException();
synchronized (cache) {
thumbnail = cache.get(uuid);
if (thumbnail != null) {
return thumbnail;
}
DownloadableFile file = getFile(message);
final String mime = file.getMimeType();
if (mime.startsWith("video/")) {
thumbnail = getVideoPreview(file, size);
} else {
Bitmap fullsize = getFullsizeImagePreview(file, size);
if (fullsize == null) {
throw new FileNotFoundException();
}
thumbnail = resize(fullsize, size);
thumbnail = rotate(thumbnail, getRotation(file));
if (mime.equals("image/gif")) {
Bitmap withGifOverlay = thumbnail.copy(Bitmap.Config.ARGB_8888,true);
drawOverlay(withGifOverlay,R.drawable.play_gif,1.0f);
thumbnail.recycle();
thumbnail = withGifOverlay;
}
}
this.mXmppConnectionService.getBitmapCache().put(uuid, thumbnail);
}
thumbnail = resize(fullsize, size);
thumbnail = rotate(thumbnail, getRotation(file));
this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),thumbnail);
}
return thumbnail;
}
private Bitmap getFullsizeImagePreview(File file, int size) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(file, size);
try {
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
} catch (OutOfMemoryError e) {
options.inSampleSize *= 2;
return BitmapFactory.decodeFile(file.getAbsolutePath(), options);
}
}
private void drawOverlay(Bitmap bitmap, int resource, float factor) {
Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setFilterBitmap(true);
paint.setDither(true);
float targetSize = Math.min(canvas.getWidth(),canvas.getHeight()) * factor;
Log.d(Config.LOGTAG,"target size overlay: "+targetSize+" overlay bitmap size was "+overlay.getHeight());
float left = (canvas.getWidth() - targetSize) / 2.0f;
float top = (canvas.getHeight() - targetSize) / 2.0f;
RectF dst = new RectF(left,top,left+targetSize-1,top+targetSize-1);
canvas.drawBitmap(overlay,null,dst,paint);
}
private Bitmap getVideoPreview(File file, int size) {
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
Bitmap frame;
try {
metadataRetriever.setDataSource(file.getAbsolutePath());
frame = metadataRetriever.getFrameAtTime(0);
metadataRetriever.release();
frame = resize(frame, size);
} catch(IllegalArgumentException | NullPointerException e) {
frame = Bitmap.createBitmap(size,size, Bitmap.Config.ARGB_8888);
frame.eraseColor(0xff000000);
}
drawOverlay(frame,R.drawable.play_video,0.75f);
return frame;
}
private static String getTakePhotoPath() {
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)+"/Camera/";
}
public Uri getTakePhotoUri() {
StringBuilder pathBuilder = new StringBuilder();
pathBuilder.append(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM));
pathBuilder.append('/');
pathBuilder.append("Camera");
pathBuilder.append('/');
pathBuilder.append("IMG_" + this.imageDateFormat.format(new Date()) + ".jpg");
Uri uri = Uri.parse("file://" + pathBuilder.toString());
File file = new File(uri.toString());
File file;
if (Config.ONLY_INTERNAL_STORAGE) {
file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath(), "Camera/IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
} else {
file = new File(getTakePhotoPath() + "IMG_" + this.IMAGE_DATE_FORMAT.format(new Date()) + ".jpg");
}
file.getParentFile().mkdirs();
return uri;
return getUriForFile(mXmppConnectionService,file);
}
public static Uri getUriForFile(Context context, File file) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N || Config.ONLY_INTERNAL_STORAGE) {
try {
String packageId = context.getPackageName();
return FileProvider.getUriForFile(context, packageId + FILE_PROVIDER, file);
} catch(IllegalArgumentException e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
throw new SecurityException();
} else {
return Uri.fromFile(file);
}
}
} else {
return Uri.fromFile(file);
}
}
public static Uri getIndexableTakePhotoUri(Uri original) {
if (Config.ONLY_INTERNAL_STORAGE || "file".equals(original.getScheme())) {
return original;
} else {
List<String> segments = original.getPathSegments();
return Uri.parse("file://"+getTakePhotoPath()+segments.get(segments.size() - 1));
}
}
public Avatar getPepAvatar(Uri image, int size, Bitmap.CompressFormat format) {
@ -320,11 +520,11 @@ public class FileBackend {
return null;
}
ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
Base64OutputStream mBase64OutputSttream = new Base64OutputStream(
Base64OutputStream mBase64OutputStream = new Base64OutputStream(
mByteArrayOutputStream, Base64.DEFAULT);
MessageDigest digest = MessageDigest.getInstance("SHA-1");
DigestOutputStream mDigestOutputStream = new DigestOutputStream(
mBase64OutputSttream, digest);
mBase64OutputStream, digest);
if (!bm.compress(format, 75, mDigestOutputStream)) {
return null;
}
@ -340,6 +540,45 @@ public class FileBackend {
}
}
public Avatar getStoredPepAvatar(String hash) {
if (hash == null) {
return null;
}
Avatar avatar = new Avatar();
File file = new File(getAvatarPath(hash));
FileInputStream is = null;
try {
avatar.size = file.length();
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
is = new FileInputStream(file);
ByteArrayOutputStream mByteArrayOutputStream = new ByteArrayOutputStream();
Base64OutputStream mBase64OutputStream = new Base64OutputStream(mByteArrayOutputStream, Base64.DEFAULT);
MessageDigest digest = MessageDigest.getInstance("SHA-1");
DigestOutputStream os = new DigestOutputStream(mBase64OutputStream, digest);
byte[] buffer = new byte[4096];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
os.flush();
os.close();
avatar.sha1sum = CryptoHelper.bytesToHex(digest.digest());
avatar.image = new String(mByteArrayOutputStream.toByteArray());
avatar.height = options.outHeight;
avatar.width = options.outWidth;
avatar.type = options.outMimeType;
return avatar;
} catch (IOException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} finally {
close(is);
}
}
public boolean isAvatarCached(Avatar avatar) {
File file = new File(getAvatarPath(avatar.getFilename()));
return file.exists();
@ -349,6 +588,7 @@ public class FileBackend {
File file;
if (isAvatarCached(avatar)) {
file = new File(getAvatarPath(avatar.getFilename()));
avatar.size = file.length();
} else {
String filename = getAvatarPath(avatar.getFilename());
file = new File(filename + ".tmp");
@ -360,7 +600,8 @@ public class FileBackend {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest);
mDigestOutputStream.write(avatar.getImageAsBytes());
final byte[] bytes = avatar.getImageAsBytes();
mDigestOutputStream.write(bytes);
mDigestOutputStream.flush();
mDigestOutputStream.close();
String sha1sum = CryptoHelper.bytesToHex(digest.digest());
@ -371,13 +612,13 @@ public class FileBackend {
file.delete();
return false;
}
avatar.size = bytes.length;
} catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
return false;
} finally {
close(os);
}
}
avatar.size = file.length();
return true;
}
@ -447,7 +688,7 @@ public class FileBackend {
Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(dest);
canvas.drawBitmap(source, null, targetRect, null);
if (source != null && !source.isRecycled()) {
if (source.isRecycled()) {
source.recycle();
}
return dest;
@ -512,37 +753,106 @@ public class FileBackend {
return inSampleSize;
}
public Uri getJingleFileUri(Message message) {
File file = getFile(message);
return Uri.parse("file://" + file.getAbsolutePath());
}
public void updateFileParams(Message message) {
updateFileParams(message,null);
}
public void updateFileParams(Message message, URL url) {
DownloadableFile file = getFile(message);
if (message.getType() == Message.TYPE_IMAGE || file.getMimeType().startsWith("image/")) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int rotation = getRotation(file);
boolean rotated = rotation == 90 || rotation == 270;
int imageHeight = rotated ? options.outWidth : options.outHeight;
int imageWidth = rotated ? options.outHeight : options.outWidth;
if (url == null) {
message.setBody(Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
} else {
message.setBody(url.toString()+"|"+Long.toString(file.getSize()) + '|' + imageWidth + '|' + imageHeight);
}
} else {
if (url != null) {
message.setBody(url.toString()+"|"+Long.toString(file.getSize()));
} else {
message.setBody(Long.toString(file.getSize()));
final String mime = file.getMimeType();
boolean image = message.getType() == Message.TYPE_IMAGE || (mime != null && mime.startsWith("image/"));
boolean video = mime != null && mime.startsWith("video/");
if (image || video) {
try {
Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file);
if (url == null) {
message.setBody(Long.toString(file.getSize()) + '|' + dimensions.width + '|' + dimensions.height);
} else {
message.setBody(url.toString() + "|" + Long.toString(file.getSize()) + '|' + dimensions.width + '|' + dimensions.height);
}
return;
} catch (NotAVideoFile notAVideoFile) {
Log.d(Config.LOGTAG,"file with mime type "+file.getMimeType()+" was not a video file");
//fall threw
}
}
if (url != null) {
message.setBody(url.toString()+"|"+Long.toString(file.getSize()));
} else {
message.setBody(Long.toString(file.getSize()));
}
}
private Dimensions getImageDimensions(File file) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int rotation = getRotation(file);
boolean rotated = rotation == 90 || rotation == 270;
int imageHeight = rotated ? options.outWidth : options.outHeight;
int imageWidth = rotated ? options.outHeight : options.outWidth;
return new Dimensions(imageHeight, imageWidth);
}
private Dimensions getVideoDimensions(File file) throws NotAVideoFile {
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
try {
metadataRetriever.setDataSource(file.getAbsolutePath());
} catch (Exception e) {
throw new NotAVideoFile();
}
String hasVideo = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO);
if (hasVideo == null) {
throw new NotAVideoFile();
}
int rotation = extractRotationFromMediaRetriever(metadataRetriever);
boolean rotated = rotation == 90 || rotation == 270;
int height;
try {
String h = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT);
height = Integer.parseInt(h);
} catch (Exception e) {
height = -1;
}
int width;
try {
String w = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH);
width = Integer.parseInt(w);
} catch (Exception e) {
width = -1;
}
metadataRetriever.release();
Log.d(Config.LOGTAG,"extracted video dims "+width+"x"+height);
return rotated ? new Dimensions(width, height) : new Dimensions(height, width);
}
private int extractRotationFromMediaRetriever(MediaMetadataRetriever metadataRetriever) {
int rotation;
if (Build.VERSION.SDK_INT >= 17) {
String r = metadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
try {
rotation = Integer.parseInt(r);
} catch (Exception e) {
rotation = 0;
}
} else {
rotation = 0;
}
return rotation;
}
private class Dimensions {
public final int width;
public final int height;
public Dimensions(int height, int width) {
this.width = width;
this.height = height;
}
}
private class NotAVideoFile extends Exception {
}
@ -591,4 +901,45 @@ public class FileBackend {
}
}
}
public static boolean weOwnFile(Context context, Uri uri) {
if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
return false;
} else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return fileIsInFilesDir(context, uri);
} else {
return weOwnFileLollipop(uri);
}
}
/**
* This is more than hacky but probably way better than doing nothing
* Further 'optimizations' might contain to get the parents of CacheDir and NoBackupDir
* and check against those as well
*/
private static boolean fileIsInFilesDir(Context context, Uri uri) {
try {
final String haystack = context.getFilesDir().getParentFile().getCanonicalPath();
final String needle = new File(uri.getPath()).getCanonicalPath();
return needle.startsWith(haystack);
} catch (IOException e) {
return false;
}
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static boolean weOwnFileLollipop(Uri uri) {
try {
File file = new File(uri.getPath());
FileDescriptor fd = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).getFileDescriptor();
StructStat st = Os.fstat(fd);
return st.st_uid == android.os.Process.myUid();
} catch (FileNotFoundException e) {
return false;
} catch (Exception e) {
return true;
}
}
}

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

@ -6,11 +6,13 @@ import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.net.Uri;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
@ -19,8 +21,10 @@ import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded;
import eu.siacs.conversations.xmpp.XmppConnection;
public class AvatarService {
public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
private static final int FG_COLOR = 0xFFFAFAFA;
private static final int TRANSPARENT = 0x00000000;
@ -40,6 +44,9 @@ public class AvatarService {
}
private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
if (contact.isSelf()) {
return get(contact.getAccount(),size,cachedOnly);
}
final String KEY = key(contact, size);
Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
if (avatar != null || cachedOnly) {
@ -60,7 +67,7 @@ public class AvatarService {
public Bitmap get(final MucOptions.User user, final int size, boolean cachedOnly) {
Contact c = user.getContact();
if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null || user.getAvatar() == null)) {
return get(c, size, cachedOnly);
} else {
return getImpl(user, size, cachedOnly);
@ -95,6 +102,9 @@ public class AvatarService {
key(contact, size));
}
}
for(Conversation conversation : mXmppConnectionService.findAllConferencesWith(contact)) {
clear(conversation);
}
}
private String key(Contact contact, int size) {
@ -162,7 +172,7 @@ public class AvatarService {
if (bitmap != null || cachedOnly) {
return bitmap;
}
final List<MucOptions.User> users = mucOptions.getUsers();
final List<MucOptions.User> users = mucOptions.getUsers(5);
int count = users.size();
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
@ -227,8 +237,7 @@ public class AvatarService {
if (avatar != null || cachedOnly) {
return avatar;
}
avatar = mXmppConnectionService.getFileBackend().getAvatar(
account.getAvatar(), size);
avatar = mXmppConnectionService.getFileBackend().getAvatar(account.getAvatar(), size);
if (avatar == null) {
avatar = get(account.getJid().toBareJid().toString(), size,false);
}
@ -243,7 +252,7 @@ public class AvatarService {
if (c != null && (c.getProfilePhoto() != null || c.getAvatar() != null)) {
return get(c, size, cachedOnly);
} else if (message.getConversation().getMode() == Conversation.MODE_MULTI){
MucOptions.User user = conversation.getMucOptions().findUser(message.getCounterpart().getResourcepart());
MucOptions.User user = conversation.getMucOptions().findUserByFullJid(message.getCounterpart());
if (user != null) {
return getImpl(user,size,cachedOnly);
}
@ -292,7 +301,7 @@ public class AvatarService {
}
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
final String trimmedName = name.trim();
final String trimmedName = name == null ? "" : name.trim();
drawTile(canvas, trimmedName, 0, 0, size, size);
mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
return bitmap;
@ -367,7 +376,7 @@ public class AvatarService {
private boolean drawTile(Canvas canvas, String name, int left, int top, int right, int bottom) {
if (name != null) {
final String letter = name.isEmpty() ? "X" : name.substring(0, 1);
final String letter = getFirstLetter(name);
final int color = UIHelper.getColorForName(name);
drawTile(canvas, letter, color, left, top, right, bottom);
return true;
@ -375,6 +384,15 @@ public class AvatarService {
return false;
}
private static String getFirstLetter(String name) {
for(Character c : name.toCharArray()) {
if (Character.isLetterOrDigit(c)) {
return c.toString();
}
}
return "X";
}
private boolean drawTile(Canvas canvas, Uri uri, int left, int top, int right, int bottom) {
if (uri != null) {
Bitmap bitmap = mXmppConnectionService.getFileBackend()
@ -387,10 +405,20 @@ public class AvatarService {
return false;
}
private boolean drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop,
int dstright, int dstbottom) {
private boolean drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop, int dstright, int dstbottom) {
Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
canvas.drawBitmap(bm, null, dst, null);
return true;
}
@Override
public void onAdvancedStreamFeaturesAvailable(Account account) {
XmppConnection.Features features = account.getXmppConnection().getFeatures();
if (features.pep() && !features.pepPersistent()) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": has pep but is not persistent");
if (account.getAvatar() != null) {
mXmppConnectionService.republishAvatarIfNeeded(account);
}
}
}
}

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

@ -52,7 +52,7 @@ public class ContactChooserTargetService extends ChooserTargetService implements
final Conversation conversation = conversations.get(i);
final String name = conversation.getName();
final Icon icon = Icon.createWithBitmap(mXmppConnectionService.getAvatarService().get(conversation, pixel));
final float score = (1.0f / MAX_TARGETS) * i;
final float score = 1 - (1.0f / MAX_TARGETS) * i;
final Bundle extras = new Bundle();
extras.putString("uuid", conversation.getUuid());
chooserTargets.add(new ChooserTarget(name, icon, score, componentName, extras));

View file

@ -3,7 +3,9 @@ package eu.siacs.conversations.services;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.net.ConnectivityManager;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.persistance.DatabaseBackend;
public class EventReceiver extends BroadcastReceiver {
@ -16,8 +18,8 @@ public class EventReceiver extends BroadcastReceiver {
} else {
mIntentForService.setAction("other");
}
if (intent.getAction().equals("ui")
|| DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
final String action = intent.getAction();
if (action.equals("ui") || DatabaseBackend.getInstance(context).hasEnabledAccounts()) {
context.startService(mIntentForService);
}
}

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);
@ -45,9 +45,9 @@ public class ExportLogsService extends Service {
new Thread(new Runnable() {
@Override
public void run() {
running.set(false);
export();
stopForeground(true);
running.set(false);
stopSelf();
}
}).start();

View file

@ -24,13 +24,13 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
private final XmppConnectionService mXmppConnectionService;
private final HashSet<Query> queries = new HashSet<Query>();
private final ArrayList<Query> pendingQueries = new ArrayList<Query>();
private final HashSet<Query> queries = new HashSet<>();
private final ArrayList<Query> pendingQueries = new ArrayList<>();
public enum PagingOrder {
NORMAL,
REVERSE
};
}
public MessageArchiveService(final XmppConnectionService service) {
this.mXmppConnectionService = service;
@ -45,8 +45,20 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
}
long startCatchup = getLastMessageTransmitted(account);
final Pair<Long,String> lastMessageReceived = mXmppConnectionService.databaseBackend.getLastMessageReceived(account);
final Pair<Long,String> lastClearDate = mXmppConnectionService.databaseBackend.getLastClearDate(account);
long startCatchup;
final String reference;
if (lastMessageReceived != null && lastMessageReceived.first >= lastClearDate.first) {
startCatchup = lastMessageReceived.first;
reference = lastMessageReceived.second;
} else {
startCatchup = lastClearDate.first;
reference = null;
}
startCatchup = Math.max(startCatchup,mXmppConnectionService.getAutomaticMessageDeletionDate());
long endCatchup = account.getXmppConnection().getLastSessionEstablished();
final Query query;
if (startCatchup == 0) {
return;
} else if (endCatchup - startCatchup >= Config.MAM_MAX_CATCHUP) {
@ -57,8 +69,11 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.query(conversation,startCatchup);
}
}
query = new Query(account, startCatchup, endCatchup);
} else {
query = new Query(account, startCatchup, endCatchup);
query.reference = reference;
}
final Query query = new Query(account, startCatchup, endCatchup);
this.queries.add(query);
this.execute(query);
}
@ -75,11 +90,6 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
private long getLastMessageTransmitted(final Account account) {
Pair<Long,String> pair = mXmppConnectionService.databaseBackend.getLastMessageReceived(account);
return pair == null ? 0 : pair.first;
}
public Query query(final Conversation conversation) {
if (conversation.getLastMessageTransmitted() < 0 && conversation.countMessages() == 0) {
return query(conversation,
@ -98,10 +108,15 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
public Query query(Conversation conversation, long start, long end) {
synchronized (this.queries) {
final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
if (start==0) {
query.reference = conversation.getFirstMamReference();
Log.d(Config.LOGTAG,"setting mam reference");
}
query.start = Math.max(start,mXmppConnectionService.getAutomaticMessageDeletionDate());
if (start > end) {
return null;
}
final Query query = new Query(conversation, start, end,PagingOrder.REVERSE);
this.queries.add(query);
this.execute(query);
return query;
@ -136,12 +151,12 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
synchronized (MessageArchiveService.this.queries) {
MessageArchiveService.this.queries.remove(query);
if (query.hasCallback()) {
query.callback();
query.callback(false);
}
}
} else if (packet.getType() != IqPacket.TYPE.RESULT) {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString());
finalizeQuery(query);
finalizeQuery(query, true);
}
}
});
@ -152,14 +167,14 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
private void finalizeQuery(Query query) {
private void finalizeQuery(Query query, boolean done) {
synchronized (this.queries) {
this.queries.remove(query);
}
final Conversation conversation = query.getConversation();
if (conversation != null) {
conversation.sort();
conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0);
conversation.setHasMessagesLeftOnServer(!done);
} else {
for(Conversation tmp : this.mXmppConnectionService.getConversations()) {
if (tmp.getAccount() == query.getAccount()) {
@ -168,7 +183,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
if (query.hasCallback()) {
query.callback();
query.callback(done);
} else {
this.mXmppConnectionService.updateConversationUi();
}
@ -188,6 +203,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
}
}
public boolean queryInProgress(Conversation conversation) {
return queryInProgress(conversation, null);
}
public void processFin(Element fin, Jid from) {
if (fin == null) {
return;
@ -202,11 +221,15 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
Element first = set == null ? null : set.findChild("first");
Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first;
boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES;
if (query.getConversation() != null) {
query.getConversation().setFirstMamReference(first == null ? null : first.getContent());
}
if (complete || relevant == null || abort) {
this.finalizeQuery(query);
Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages");
final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() <= mXmppConnectionService.getAutomaticMessageDeletionDate();
this.finalizeQuery(query, done);
Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done));
if (query.getWith() == null && query.getMessageCount() > 0) {
mXmppConnectionService.getNotificationService().finishBacklog(true);
mXmppConnectionService.getNotificationService().finishBacklog(true,query.getAccount());
}
} else {
final Query nextQuery;
@ -216,9 +239,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
nextQuery = query.prev(first == null ? null : first.getContent());
}
this.execute(nextQuery);
this.finalizeQuery(query);
this.finalizeQuery(query, false);
synchronized (this.queries) {
this.queries.remove(query);
this.queries.add(nextQuery);
}
}
@ -274,7 +296,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.end = end;
this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32);
}
private Query page(String reference) {
Query query = new Query(this.account,this.start,this.end);
query.reference = reference;
@ -324,10 +346,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
this.callback = callback;
}
public void callback() {
public void callback(boolean done) {
if (this.callback != null) {
this.callback.onMoreMessagesLoaded(messageCount,conversation);
if (messageCount == 0) {
if (done) {
this.callback.informUser(R.string.no_more_history_on_server);
}
}
@ -345,12 +367,9 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
return this.account;
}
public void incrementTotalCount() {
this.totalCount++;
}
public void incrementMessageCount() {
this.messageCount++;
this.totalCount++;
}
public int getTotalCount() {
@ -373,7 +392,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded {
public String toString() {
StringBuilder builder = new StringBuilder();
if (this.muc()) {
builder.append("to="+this.getWith().toString());
builder.append("to=");
builder.append(this.getWith().toString());
} else {
builder.append("with=");
if (this.getWith() == null) {

View file

@ -1,9 +1,7 @@
package eu.siacs.conversations.services;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
@ -13,35 +11,37 @@ import android.os.SystemClock;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigPictureStyle;
import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.app.TaskStackBuilder;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.app.RemoteInput;
import android.text.Html;
import android.util.DisplayMetrics;
import org.json.JSONArray;
import org.json.JSONObject;
import android.util.Log;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.ManageAccountActivity;
import eu.siacs.conversations.ui.SettingsActivity;
import eu.siacs.conversations.ui.TimePreference;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper;
public class NotificationService {
private static final String CONVERSATIONS_GROUP = "eu.siacs.conversations";
private final XmppConnectionService mXmppConnectionService;
private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>();
@ -66,28 +66,6 @@ public class NotificationService {
);
}
public void notifyPebble(final Message message) {
final Intent i = new Intent("com.getpebble.action.SEND_NOTIFICATION");
final Conversation conversation = message.getConversation();
final JSONObject jsonData = new JSONObject(new HashMap<String, String>(2) {{
put("title", conversation.getName());
put("body", message.getBody());
}});
final String notificationData = new JSONArray().put(jsonData).toString();
i.putExtra("messageType", "PEBBLE_ALERT");
i.putExtra("sender", "Conversations"); /* XXX: Shouldn't be hardcoded, e.g., AbstractGenerator.APP_NAME); */
i.putExtra("notificationData", notificationData);
// notify Pebble App
i.setPackage("com.getpebble.android");
mXmppConnectionService.sendBroadcast(i);
// notify Gadgetbridge
i.setPackage("nodomain.freeyourgadget.gadgetbridge");
mXmppConnectionService.sendBroadcast(i);
}
public boolean notificationsEnabled() {
return mXmppConnectionService.getPreferences().getBoolean("show_notification", true);
}
@ -115,13 +93,35 @@ public class NotificationService {
}
}
public void finishBacklog(boolean notify) {
public void pushFromDirectReply(final Message message) {
synchronized (notifications) {
pushToStack(message);
updateNotification(false);
}
}
public void finishBacklog(boolean notify, Account account) {
synchronized (notifications) {
mXmppConnectionService.updateUnreadCountBadge();
updateNotification(notify);
if (account == null || !notify) {
updateNotification(notify);
} else {
boolean hasPendingMessages = false;
for(ArrayList<Message> messages : notifications.values()) {
if (messages.size() > 0 && messages.get(0).getConversation().getAccount() == account) {
hasPendingMessages = true;
break;
}
}
updateNotification(hasPendingMessages);
}
}
}
public void finishBacklog(boolean notify) {
finishBacklog(notify,null);
}
private void pushToStack(final Message message) {
final String conversationUuid = message.getConversationUuid();
if (notifications.containsKey(conversationUuid)) {
@ -136,10 +136,12 @@ public class NotificationService {
public void push(final Message message) {
mXmppConnectionService.updateUnreadCountBadge();
if (!notify(message)) {
Log.d(Config.LOGTAG,message.getConversation().getAccount().getJid().toBareJid()+": suppressing notification because turned off");
return;
}
final boolean isScreenOn = mXmppConnectionService.isInteractive();
if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) {
Log.d(Config.LOGTAG,message.getConversation().getAccount().getJid().toBareJid()+": suppressing notification because conversation is open");
return;
}
synchronized (notifications) {
@ -149,14 +151,14 @@ public class NotificationService {
&& !account.inGracePeriod()
&& !this.inMiniGracePeriod(account);
updateNotification(doNotify);
if (doNotify) {
notifyPebble(message);
}
}
}
public void clear() {
synchronized (notifications) {
for(ArrayList<Message> messages : notifications.values()) {
markAsReadIfHasDirectReply(messages);
}
notifications.clear();
updateNotification(false);
}
@ -164,23 +166,35 @@ public class NotificationService {
public void clear(final Conversation conversation) {
synchronized (notifications) {
markAsReadIfHasDirectReply(conversation);
notifications.remove(conversation.getUuid());
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
notificationManager.cancel(conversation.getUuid(), NOTIFICATION_ID);
updateNotification(false);
}
}
private void markAsReadIfHasDirectReply(final Conversation conversation) {
markAsReadIfHasDirectReply(notifications.get(conversation.getUuid()));
}
private void markAsReadIfHasDirectReply(final ArrayList<Message> messages) {
if (messages != null && messages.size() > 0) {
Message last = messages.get(messages.size() - 1);
if (last.getStatus() != Message.STATUS_RECEIVED) {
mXmppConnectionService.markRead(last.getConversation(), false);
}
}
}
private void setNotificationColor(final Builder mBuilder) {
mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary));
mBuilder.setColor(mXmppConnectionService.getResources().getColor(R.color.primary500));
}
public void updateNotification(final boolean notify) {
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
.getSystemService(Context.NOTIFICATION_SERVICE);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
final SharedPreferences preferences = mXmppConnectionService.getPreferences();
final String ringtone = preferences.getString("notification_ringtone", null);
final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true);
if (notifications.size() == 0) {
notificationManager.cancel(NOTIFICATION_ID);
} else {
@ -188,31 +202,46 @@ public class NotificationService {
this.markLastNotification();
}
final Builder mBuilder;
if (notifications.size() == 1) {
mBuilder = buildSingleConversations(notify);
if (notifications.size() == 1 && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
mBuilder = buildSingleConversations(notifications.values().iterator().next());
modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
} else {
mBuilder = buildMultipleConversation();
}
if (notify && !isQuietHours()) {
if (vibrate) {
final int dat = 70;
final long[] pattern = {0, 3 * dat, dat, dat};
mBuilder.setVibrate(pattern);
}
if (ringtone != null) {
mBuilder.setSound(Uri.parse(ringtone));
modifyForSoundVibrationAndLight(mBuilder, notify, preferences);
notificationManager.notify(NOTIFICATION_ID, mBuilder.build());
for(Map.Entry<String,ArrayList<Message>> entry : notifications.entrySet()) {
Builder singleBuilder = buildSingleConversations(entry.getValue());
singleBuilder.setGroup(CONVERSATIONS_GROUP);
modifyForSoundVibrationAndLight(singleBuilder,notify,preferences);
notificationManager.notify(entry.getKey(), NOTIFICATION_ID ,singleBuilder.build());
}
}
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
}
}
private void modifyForSoundVibrationAndLight(Builder mBuilder, boolean notify, SharedPreferences preferences) {
final String ringtone = preferences.getString("notification_ringtone", null);
final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true);
final boolean led = preferences.getBoolean("led", true);
if (notify && !isQuietHours()) {
if (vibrate) {
final int dat = 70;
final long[] pattern = {0, 3 * dat, dat, dat};
mBuilder.setVibrate(pattern);
}
setNotificationColor(mBuilder);
mBuilder.setDefaults(0);
mBuilder.setSmallIcon(R.drawable.ic_notification);
mBuilder.setDeleteIntent(createDeleteIntent());
if (ringtone != null) {
mBuilder.setSound(Uri.parse(ringtone));
}
}
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mBuilder.setCategory(Notification.CATEGORY_MESSAGE);
}
setNotificationColor(mBuilder);
mBuilder.setDefaults(0);
if (led) {
mBuilder.setLights(0xff00FF00, 2000, 3000);
final Notification notification = mBuilder.build();
notificationManager.notify(NOTIFICATION_ID, notification);
}
}
@ -253,13 +282,15 @@ public class NotificationService {
if (conversation != null) {
mBuilder.setContentIntent(createContentIntent(conversation));
}
mBuilder.setGroupSummary(true);
mBuilder.setGroup(CONVERSATIONS_GROUP);
mBuilder.setDeleteIntent(createDeleteIntent(null));
mBuilder.setSmallIcon(R.drawable.ic_notification);
return mBuilder;
}
private Builder buildSingleConversations(final boolean notify) {
final Builder mBuilder = new NotificationCompat.Builder(
mXmppConnectionService);
final ArrayList<Message> messages = notifications.values().iterator().next();
private Builder buildSingleConversations(final ArrayList<Message> messages) {
final Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
if (messages.size() >= 1) {
final Conversation conversation = messages.get(0).getConversation();
mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
@ -271,11 +302,16 @@ public class NotificationService {
} else {
Message message;
if ((message = getImage(messages)) != null) {
modifyForImage(mBuilder, message, messages, notify);
} else if (conversation.getMode() == Conversation.MODE_MULTI) {
modifyForConference(mBuilder, conversation, messages, notify);
modifyForImage(mBuilder, message, messages);
} else {
modifyForTextOnly(mBuilder, messages, notify);
modifyForTextOnly(mBuilder, messages);
}
RemoteInput remoteInput = new RemoteInput.Builder("text_reply").setLabel(UIHelper.getMessageHint(mXmppConnectionService, conversation)).build();
NotificationCompat.Action replyAction = new NotificationCompat.Action.Builder(R.drawable.ic_send_text_offline, "Reply", createReplyIntent(conversation, false)).addRemoteInput(remoteInput).build();
NotificationCompat.Action wearReplyAction = new NotificationCompat.Action.Builder(R.drawable.ic_send_text_offline, "Reply", createReplyIntent(conversation, true)).addRemoteInput(remoteInput).build();
mBuilder.extend(new NotificationCompat.WearableExtender().addAction(wearReplyAction));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mBuilder.addAction(replyAction);
}
if ((message = getFirstDownloadableMessage(messages)) != null) {
mBuilder.addAction(
@ -292,13 +328,23 @@ public class NotificationService {
createShowLocationIntent(message));
}
}
if (conversation.getMode() == Conversation.MODE_SINGLE) {
Contact contact = conversation.getContact();
Uri systemAccount = contact.getSystemAccount();
if (systemAccount != null) {
mBuilder.addPerson(systemAccount.toString());
}
}
mBuilder.setWhen(conversation.getLatestMessage().getTimeSent());
mBuilder.setSmallIcon(R.drawable.ic_notification);
mBuilder.setDeleteIntent(createDeleteIntent(conversation));
mBuilder.setContentIntent(createContentIntent(conversation));
}
return mBuilder;
}
private void modifyForImage(final Builder builder, final Message message,
final ArrayList<Message> messages, final boolean notify) {
final ArrayList<Message> messages) {
try {
final Bitmap bitmap = mXmppConnectionService.getFileBackend()
.getThumbnail(message, getPixel(288), false);
@ -312,8 +358,9 @@ public class NotificationService {
final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
bigPictureStyle.bigPicture(bitmap);
if (tmp.size() > 0) {
bigPictureStyle.setSummaryText(getMergedBodies(tmp));
builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, tmp.get(0)).first);
CharSequence text = getMergedBodies(tmp);
bigPictureStyle.setSummaryText(text);
builder.setContentText(text);
} else {
builder.setContentText(mXmppConnectionService.getString(
R.string.received_x_file,
@ -321,55 +368,50 @@ public class NotificationService {
}
builder.setStyle(bigPictureStyle);
} catch (final FileNotFoundException e) {
modifyForTextOnly(builder, messages, notify);
modifyForTextOnly(builder, messages);
}
}
private void modifyForTextOnly(final Builder builder,
final ArrayList<Message> messages, final boolean notify) {
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(0)).first);
if (notify) {
builder.setTicker(UIHelper.getMessagePreview(mXmppConnectionService, messages.get(messages.size() - 1)).first);
}
}
private void modifyForConference(Builder builder, Conversation conversation, List<Message> messages, boolean notify) {
final Message first = messages.get(0);
final Message last = messages.get(messages.size() - 1);
final NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
style.setBigContentTitle(conversation.getName());
for(Message message : messages) {
if (message.hasMeCommand()) {
style.addLine(UIHelper.getMessagePreview(mXmppConnectionService,message).first);
} else {
style.addLine(Html.fromHtml("<b>" + UIHelper.getMessageDisplayName(message) + "</b>: " + UIHelper.getMessagePreview(mXmppConnectionService, message).first));
private void modifyForTextOnly(final Builder builder, final ArrayList<Message> messages) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
NotificationCompat.MessagingStyle messagingStyle = new NotificationCompat.MessagingStyle(mXmppConnectionService.getString(R.string.me));
Conversation conversation = messages.get(0).getConversation();
if (conversation.getMode() == Conversation.MODE_MULTI) {
messagingStyle.setConversationTitle(conversation.getName());
}
}
builder.setContentText((first.hasMeCommand() ? "" :UIHelper.getMessageDisplayName(first)+ ": ") +UIHelper.getMessagePreview(mXmppConnectionService, first).first);
builder.setStyle(style);
if (notify) {
builder.setTicker((last.hasMeCommand() ? "" : UIHelper.getMessageDisplayName(last) + ": ") + UIHelper.getMessagePreview(mXmppConnectionService,last).first);
for (Message message : messages) {
String sender = message.getStatus() == Message.STATUS_RECEIVED ? UIHelper.getMessageDisplayName(message) : null;
messagingStyle.addMessage(UIHelper.getMessagePreview(mXmppConnectionService,message).first, message.getTimeSent(), sender);
}
builder.setStyle(messagingStyle);
} else {
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(getMergedBodies(messages)));
builder.setContentText(UIHelper.getMessagePreview(mXmppConnectionService, messages.get((messages.size()-1))).first);
}
}
private Message getImage(final Iterable<Message> messages) {
Message image = null;
for (final Message message : messages) {
if (message.getStatus() != Message.STATUS_RECEIVED) {
return null;
}
if (message.getType() != Message.TYPE_TEXT
&& message.getTransferable() == null
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& message.getFileParams().height > 0) {
return message;
image = message;
}
}
return null;
return image;
}
private Message getFirstDownloadableMessage(final Iterable<Message> messages) {
for (final Message message : messages) {
if ((message.getType() == Message.TYPE_FILE || message.getType() == Message.TYPE_IMAGE) &&
message.getTransferable() != null) {
if (message.getTransferable() != null
&& (message.getType() == Message.TYPE_FILE
|| message.getType() == Message.TYPE_IMAGE
|| message.treatAsDownloadable() != Message.Decision.NEVER)) {
return message;
}
}
@ -407,28 +449,21 @@ public class NotificationService {
}
private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) {
final TaskStackBuilder stackBuilder = TaskStackBuilder
.create(mXmppConnectionService);
stackBuilder.addParentStack(ConversationActivity.class);
final Intent viewConversationIntent = new Intent(mXmppConnectionService,
ConversationActivity.class);
final Intent viewConversationIntent = new Intent(mXmppConnectionService,ConversationActivity.class);
viewConversationIntent.setAction(ConversationActivity.ACTION_VIEW_CONVERSATION);
viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid);
if (downloadMessageUuid != null) {
viewConversationIntent.setAction(ConversationActivity.ACTION_DOWNLOAD);
viewConversationIntent.putExtra(ConversationActivity.EXTRA_DOWNLOAD_UUID, downloadMessageUuid);
return PendingIntent.getActivity(mXmppConnectionService,
conversationUuid.hashCode() % 389782,
viewConversationIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
} else {
viewConversationIntent.setAction(Intent.ACTION_VIEW);
return PendingIntent.getActivity(mXmppConnectionService,
conversationUuid.hashCode() % 936236,
viewConversationIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
if (conversationUuid != null) {
viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid);
viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
}
if (downloadMessageUuid != null) {
viewConversationIntent.putExtra(ConversationActivity.MESSAGE, downloadMessageUuid);
}
stackBuilder.addNextIntent(viewConversationIntent);
return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
}
private PendingIntent createDownloadIntent(final Message message) {
@ -439,13 +474,25 @@ public class NotificationService {
return createContentIntent(conversation.getUuid(), null);
}
private PendingIntent createDeleteIntent() {
final Intent intent = new Intent(mXmppConnectionService,
XmppConnectionService.class);
private PendingIntent createDeleteIntent(Conversation conversation) {
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION);
if (conversation != null) {
intent.putExtra("uuid", conversation.getUuid());
return PendingIntent.getService(mXmppConnectionService, conversation.getUuid().hashCode() % 247527, intent, 0);
}
return PendingIntent.getService(mXmppConnectionService, 0, intent, 0);
}
private PendingIntent createReplyIntent(Conversation conversation, boolean dismissAfterReply) {
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
intent.setAction(XmppConnectionService.ACTION_REPLY_TO_CONVERSATION);
intent.putExtra("uuid",conversation.getUuid());
intent.putExtra("dismiss_notification",dismissAfterReply);
int id = conversation.getUuid().hashCode() % (dismissAfterReply ? 402359 : 426583);
return PendingIntent.getService(mXmppConnectionService, id, intent, 0);
}
private PendingIntent createDisableForeground() {
final Intent intent = new Intent(mXmppConnectionService,
XmppConnectionService.class);
@ -459,11 +506,10 @@ public class NotificationService {
return PendingIntent.getService(mXmppConnectionService, 45, intent, 0);
}
private PendingIntent createDisableAccountIntent(final Account account) {
private PendingIntent createDismissErrorIntent() {
final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class);
intent.setAction(XmppConnectionService.ACTION_DISABLE_ACCOUNT);
intent.putExtra("account", account.getJid().toBareJid().toString());
return PendingIntent.getService(mXmppConnectionService, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
intent.setAction(XmppConnectionService.ACTION_DISMISS_ERROR_NOTIFICATIONS);
return PendingIntent.getService(mXmppConnectionService, 69, intent, 0);
}
private boolean wasHighlightedOrPrivate(final Message message) {
@ -476,7 +522,7 @@ public class NotificationService {
return (m.find() || message.getType() == Message.TYPE_PRIVATE);
}
private static Pattern generateNickHighlightPattern(final String nick) {
public static Pattern generateNickHighlightPattern(final String nick) {
// We expect a word boundary, i.e. space or start of string, followed by
// the
// nick (matched in case-insensitive manner), followed by optional
@ -533,17 +579,19 @@ public class NotificationService {
mBuilder.setContentIntent(createOpenConversationsIntent());
mBuilder.setWhen(0);
mBuilder.setPriority(Config.SHOW_CONNECTED_ACCOUNTS ? NotificationCompat.PRIORITY_DEFAULT : NotificationCompat.PRIORITY_MIN);
final int cancelIcon;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mBuilder.setCategory(Notification.CATEGORY_SERVICE);
cancelIcon = R.drawable.ic_cancel_white_24dp;
} else {
cancelIcon = R.drawable.ic_action_cancel;
}
mBuilder.setSmallIcon(R.drawable.ic_link_white_24dp);
mBuilder.addAction(cancelIcon,
mXmppConnectionService.getString(R.string.disable_foreground_service),
createDisableForeground());
if (Config.SHOW_DISABLE_FOREGROUND) {
final int cancelIcon;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mBuilder.setCategory(Notification.CATEGORY_SERVICE);
cancelIcon = R.drawable.ic_cancel_white_24dp;
} else {
cancelIcon = R.drawable.ic_action_cancel;
}
mBuilder.addAction(cancelIcon,
mXmppConnectionService.getString(R.string.disable_foreground_service),
createDisableForeground());
}
return mBuilder.build();
}
@ -552,14 +600,14 @@ public class NotificationService {
}
public void updateErrorNotification() {
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(mXmppConnectionService);
final List<Account> errors = new ArrayList<>();
for (final Account account : mXmppConnectionService.getAccounts()) {
if (account.hasErrorStatus()) {
if (account.hasErrorStatus() && account.showErrorNotification()) {
errors.add(account);
}
}
if (mXmppConnectionService.getPreferences().getBoolean("keep_foreground_service", false)) {
if (mXmppConnectionService.getPreferences().getBoolean(SettingsActivity.KEEP_FOREGROUND_SERVICE, false)) {
notificationManager.notify(FOREGROUND_NOTIFICATION_ID, createForegroundNotification());
}
final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService);
@ -576,27 +624,17 @@ public class NotificationService {
mBuilder.addAction(R.drawable.ic_autorenew_white_24dp,
mXmppConnectionService.getString(R.string.try_again),
createTryAgainIntent());
if (errors.size() == 1) {
mBuilder.addAction(R.drawable.ic_block_white_24dp,
mXmppConnectionService.getString(R.string.disable_account),
createDisableAccountIntent(errors.get(0)));
}
mBuilder.setOngoing(true);
//mBuilder.setLights(0xffffffff, 2000, 4000);
mBuilder.setDeleteIntent(createDismissErrorIntent());
mBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
mBuilder.setSmallIcon(R.drawable.ic_warning_white_24dp);
} else {
mBuilder.setSmallIcon(R.drawable.ic_stat_alert_warning);
}
final TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService);
stackBuilder.addParentStack(ConversationActivity.class);
final Intent manageAccountsIntent = new Intent(mXmppConnectionService, ManageAccountActivity.class);
stackBuilder.addNextIntent(manageAccountsIntent);
final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
mBuilder.setContentIntent(resultPendingIntent);
mBuilder.setContentIntent(PendingIntent.getActivity(mXmppConnectionService,
145,
new Intent(mXmppConnectionService,ManageAccountActivity.class),
PendingIntent.FLAG_UPDATE_CURRENT));
notificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build());
}
}

View file

@ -1,7 +1,9 @@
package eu.siacs.conversations.ui;
import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.PreferenceManager;
import eu.siacs.conversations.R;
@ -10,6 +12,12 @@ public class AboutActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Boolean dark = PreferenceManager.getDefaultSharedPreferences(getApplicationContext())
.getString("theme", "light").equals("dark");
int mTheme = dark ? R.style.ConversationsTheme_Dark : R.style.ConversationsTheme;
setTheme(mTheme);
setContentView(R.layout.activity_about);
}
}

View file

@ -3,6 +3,14 @@ package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.TypefaceSpan;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Blockable;
@ -15,16 +23,30 @@ public final class BlockContactDialog {
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
final boolean isBlocked = blockable.isBlocked();
builder.setNegativeButton(R.string.cancel, null);
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
LinearLayout view = (LinearLayout) inflater.inflate(R.layout.dialog_block_contact,null);
TextView message = (TextView) view.findViewById(R.id.text);
final CheckBox report = (CheckBox) view.findViewById(R.id.report_spam);
final boolean reporting = blockable.getAccount().getXmppConnection().getFeatures().spamReporting();
report.setVisibility(!isBlocked && reporting ? View.VISIBLE : View.GONE);
builder.setView(view);
String value;
SpannableString spannable;
if (blockable.getJid().isDomainJid() || blockable.getAccount().isBlocked(blockable.getJid().toDomainJid())) {
builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain);
builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text,
blockable.getJid().toDomainJid()));
value = blockable.getJid().toDomainJid().toString();
spannable = new SpannableString(context.getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text, value));
} else {
builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact);
builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text,
blockable.getJid().toBareJid()));
value = blockable.getJid().toBareJid().toString();
spannable = new SpannableString(context.getString(isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text, value));
}
int start = spannable.toString().indexOf(value);
if (start >= 0) {
spannable.setSpan(new TypefaceSpan("monospace"),start,start + value.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
message.setText(spannable);
builder.setPositiveButton(isBlocked ? R.string.unblock : R.string.block, new DialogInterface.OnClickListener() {
@Override
@ -32,7 +54,7 @@ public final class BlockContactDialog {
if (isBlocked) {
xmppConnectionService.sendUnblockRequest(blockable);
} else {
xmppConnectionService.sendBlockRequest(blockable);
xmppConnectionService.sendBlockRequest(blockable, report.isChecked());
}
}
});

View file

@ -49,7 +49,7 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
if (account != null) {
for (final Jid jid : account.getBlocklist()) {
final Contact contact = account.getRoster().getContact(jid);
if (contact.match(needle) && contact.isBlocked()) {
if (contact.match(this, needle) && contact.isBlocked()) {
getListItems().add(contact);
}
}

View file

@ -1,9 +1,11 @@
package eu.siacs.conversations.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import eu.siacs.conversations.R;
@ -22,7 +24,7 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
final String currentPassword = mCurrentPassword.getText().toString();
final String newPassword = mNewPassword.getText().toString();
final String newPasswordConfirm = mNewPasswordConfirm.getText().toString();
if (!currentPassword.equals(mAccount.getPassword())) {
if (!mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE) && !currentPassword.equals(mAccount.getPassword())) {
mCurrentPassword.requestFocus();
mCurrentPassword.setError(getString(R.string.account_status_unauthorized));
} else if (!newPassword.equals(newPasswordConfirm)) {
@ -43,6 +45,7 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
}
}
};
private TextView mCurrentPasswordLabel;
private EditText mCurrentPassword;
private EditText mNewPassword;
private EditText mNewPasswordConfirm;
@ -51,7 +54,13 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
@Override
void onBackendConnected() {
this.mAccount = extractAccount(getIntent());
if (this.mAccount != null && this.mAccount.isOptionSet(Account.OPTION_MAGIC_CREATE)) {
this.mCurrentPasswordLabel.setVisibility(View.GONE);
this.mCurrentPassword.setVisibility(View.GONE);
} else {
this.mCurrentPasswordLabel.setVisibility(View.VISIBLE);
this.mCurrentPassword.setVisibility(View.VISIBLE);
}
}
@Override
@ -67,11 +76,23 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
});
this.mChangePasswordButton = (Button) findViewById(R.id.right_button);
this.mChangePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked);
this.mCurrentPasswordLabel = (TextView) findViewById(R.id.current_password_label);
this.mCurrentPassword = (EditText) findViewById(R.id.current_password);
this.mNewPassword = (EditText) findViewById(R.id.new_password);
this.mNewPasswordConfirm = (EditText) findViewById(R.id.new_password_confirm);
}
@Override
protected void onStart() {
super.onStart();
Intent intent = getIntent();
String password = intent != null ? intent.getStringExtra("password") : null;
if (password != null) {
this.mNewPassword.getEditableText().clear();
this.mNewPassword.getEditableText().append(password);
}
}
@Override
public void onPasswordChangeSucceeded() {
runOnUiThread(new Runnable() {

View file

@ -1,8 +1,10 @@
package eu.siacs.conversations.ui;
import android.app.ActionBar;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.StringRes;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
@ -32,6 +34,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
private Set<Contact> selected;
private Set<String> filterContacts;
public static final String EXTRA_TITLE_RES_ID = "extra_title_res_id";
@Override
public void onCreate(final Bundle savedInstanceState) {
@ -77,6 +80,8 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
String[] selection = getSelectedContactJids();
data.putExtra("contacts", selection);
data.putExtra("multiple", true);
data.putExtra(EXTRA_ACCOUNT,request.getStringExtra(EXTRA_ACCOUNT));
data.putExtra("subject", request.getStringExtra("subject"));
setResult(RESULT_OK, data);
finish();
return true;
@ -121,6 +126,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
data.putExtra("conversation",
request.getStringExtra("conversation"));
data.putExtra("multiple", false);
data.putExtra("subject", request.getStringExtra("subject"));
setResult(RESULT_OK, data);
finish();
}
@ -128,6 +134,22 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
}
@Override
public void onStart() {
super.onStart();
Intent intent = getIntent();
@StringRes
int res = intent != null ? intent.getIntExtra(EXTRA_TITLE_RES_ID,R.string.title_activity_choose_contact) : R.string.title_activity_choose_contact;
ActionBar bar = getActionBar();
if (bar != null) {
try {
bar.setTitle(res);
} catch (Exception e) {
bar.setTitle(R.string.title_activity_choose_contact);
}
}
}
@Override
public boolean onCreateOptionsMenu(final Menu menu) {
super.onCreateOptionsMenu(menu);
@ -144,7 +166,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
for (final Contact contact : account.getRoster().getContacts()) {
if (contact.showInRoster() &&
!filterContacts.contains(contact.getJid().toBareJid().toString())
&& contact.match(needle)) {
&& contact.match(this, needle)) {
getListItems().add(contact);
}
}
@ -194,6 +216,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
data.putExtra("conversation",
request.getStringExtra("conversation"));
data.putExtra("multiple", false);
data.putExtra("subject", request.getStringExtra("subject"));
setResult(RESULT_OK, data);
finish();

View file

@ -1,13 +1,10 @@
package eu.siacs.conversations.ui;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.DialogInterface;
import android.content.IntentSender.SendIntentException;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.LayoutInflater;
@ -27,7 +24,6 @@ import org.openintents.openpgp.util.OpenPgpUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicInteger;
import eu.siacs.conversations.Config;
@ -46,6 +42,9 @@ import eu.siacs.conversations.xmpp.jid.Jid;
public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed {
public static final String ACTION_VIEW_MUC = "view_muc";
private static final float INACTIVE_ALPHA = 0.4684f; //compromise between dark and light theme
private Conversation mConversation;
private OnClickListener inviteListener = new OnClickListener() {
@ -253,6 +252,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
@Override
public void onClick(View v) {
quickEdit(mConversation.getMucOptions().getActualNick(),
0,
new OnValueEdited() {
@Override
@ -271,6 +271,15 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.mNotifyStatusText = (TextView) findViewById(R.id.notification_status_text);
}
@Override
protected void onStart() {
super.onStart();
final int theme = findTheme();
if (this.mTheme != theme) {
recreate();
}
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
switch (menuItem.getItemId()) {
@ -279,9 +288,14 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
break;
case R.id.action_edit_subject:
if (mConversation != null) {
quickEdit(mConversation.getName(),this.onSubjectEdited);
quickEdit(mConversation.getMucOptions().getSubject(),
R.string.edit_subject_hint,
this.onSubjectEdited);
}
break;
case R.id.action_share:
shareUri();
break;
case R.id.action_save_as_bookmark:
saveAsBookmark();
break;
@ -334,7 +348,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.muc_details, menu);
return true;
return super.onCreateOptionsMenu(menu);
}
@Override
@ -349,13 +363,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
final Contact contact = user.getContact();
if (contact != null) {
name = contact.getDisplayName();
} else if (user.getJid() != null){
name = user.getJid().toBareJid().toString();
} else if (user.getRealJid() != null){
name = user.getRealJid().toBareJid().toString();
} else {
name = user.getName();
}
menu.setHeaderTitle(name);
if (user.getJid() != null) {
if (user.getRealJid() != null) {
MenuItem showContactDetails = menu.findItem(R.id.action_contact_details);
MenuItem startConversation = menu.findItem(R.id.start_conversation);
MenuItem giveMembership = menu.findItem(R.id.give_membership);
@ -364,9 +378,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
MenuItem removeAdminPrivileges = menu.findItem(R.id.remove_admin_privileges);
MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room);
MenuItem banFromConference = menu.findItem(R.id.ban_from_conference);
MenuItem invite = menu.findItem(R.id.invite);
startConversation.setVisible(true);
if (contact != null) {
showContactDetails.setVisible(true);
showContactDetails.setVisible(!contact.isSelf());
}
if (user.getRole() == MucOptions.Role.NONE) {
invite.setVisible(true);
}
if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) &&
self.getAffiliation().outranks(user.getAffiliation())) {
@ -388,7 +406,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
} else {
MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message);
sendPrivateMessage.setVisible(true);
sendPrivateMessage.setVisible(user.getRole().ranks(MucOptions.Role.PARTICIPANT));
}
}
@ -397,6 +415,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
@Override
public boolean onContextItemSelected(MenuItem item) {
Jid jid = mSelectedUser.getRealJid();
switch (item.getItemId()) {
case R.id.action_contact_details:
Contact contact = mSelectedUser.getContact();
@ -408,27 +427,32 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
startConversation(mSelectedUser);
return true;
case R.id.give_admin_privileges:
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.ADMIN,this);
xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.ADMIN,this);
return true;
case R.id.give_membership:
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this);
xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.MEMBER,this);
return true;
case R.id.remove_membership:
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.NONE,this);
xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.NONE,this);
return true;
case R.id.remove_admin_privileges:
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.MEMBER,this);
xmppConnectionService.changeAffiliationInConference(mConversation, jid, MucOptions.Affiliation.MEMBER,this);
return true;
case R.id.remove_from_room:
removeFromRoom(mSelectedUser);
return true;
case R.id.ban_from_conference:
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this);
xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,this);
xmppConnectionService.changeAffiliationInConference(mConversation,jid, MucOptions.Affiliation.OUTCAST,this);
if (mSelectedUser.getRole() != MucOptions.Role.NONE) {
xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, this);
}
return true;
case R.id.send_private_message:
privateMsgInMuc(mConversation,mSelectedUser.getName());
return true;
case R.id.invite:
xmppConnectionService.directInvite(mConversation, jid);
return true;
default:
return super.onContextItemSelected(item);
}
@ -436,8 +460,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
private void removeFromRoom(final User user) {
if (mConversation.getMucOptions().membersOnly()) {
xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.NONE,this);
xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this);
xmppConnectionService.changeAffiliationInConference(mConversation,user.getRealJid(), MucOptions.Affiliation.NONE,this);
if (user.getRole() != MucOptions.Role.NONE) {
xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, ConferenceDetailsActivity.this);
}
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.ban_from_conference);
@ -446,8 +472,10 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
builder.setPositiveButton(R.string.ban_now,new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
xmppConnectionService.changeAffiliationInConference(mConversation,user.getJid(), MucOptions.Affiliation.OUTCAST,ConferenceDetailsActivity.this);
xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,ConferenceDetailsActivity.this);
xmppConnectionService.changeAffiliationInConference(mConversation,user.getRealJid(), MucOptions.Affiliation.OUTCAST,ConferenceDetailsActivity.this);
if (user.getRole() != MucOptions.Role.NONE) {
xmppConnectionService.changeRoleInConference(mConversation, mSelectedUser.getName(), MucOptions.Role.NONE, ConferenceDetailsActivity.this);
}
}
});
builder.create().show();
@ -455,23 +483,15 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
protected void startConversation(User user) {
if (user.getJid() != null) {
Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(),user.getJid().toBareJid(),false);
if (user.getRealJid() != null) {
Conversation conversation = xmppConnectionService.findOrCreateConversation(this.mConversation.getAccount(),user.getRealJid().toBareJid(),false);
switchToConversation(conversation);
}
}
protected void saveAsBookmark() {
Account account = mConversation.getAccount();
Bookmark bookmark = new Bookmark(account, mConversation.getJid().toBareJid());
if (!mConversation.getJid().isBareJid()) {
bookmark.setNick(mConversation.getJid().getResourcepart());
}
bookmark.setBookmarkName(mConversation.getMucOptions().getSubject());
bookmark.setAutojoin(getPreferences().getBoolean("autojoin",true));
account.getBookmarks().add(bookmark);
xmppConnectionService.pushBookmarks(account);
mConversation.setBookmark(bookmark);
xmppConnectionService.saveConversationAsBookmark(mConversation,
mConversation.getMucOptions().getSubject());
}
protected void deleteBookmark() {
@ -492,8 +512,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.uuid = getIntent().getExtras().getString("uuid");
}
if (uuid != null) {
this.mConversation = xmppConnectionService
.findConversationByUuid(uuid);
this.mConversation = xmppConnectionService.findConversationByUuid(uuid);
if (this.mConversation != null) {
updateView();
}
@ -501,6 +520,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
private void updateView() {
invalidateOptionsMenu();
final MucOptions mucOptions = mConversation.getMucOptions();
final User self = mucOptions.getSelf();
String account;
@ -541,30 +561,30 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
}
int ic_notifications = getThemeResource(R.attr.icon_notifications, R.drawable.ic_notifications_black54_24dp);
int ic_notifications_off = getThemeResource(R.attr.icon_notifications_off, R.drawable.ic_notifications_off_black54_24dp);
int ic_notifications_paused = getThemeResource(R.attr.icon_notifications_paused, R.drawable.ic_notifications_paused_black54_24dp);
int ic_notifications_none = getThemeResource(R.attr.icon_notifications_none, R.drawable.ic_notifications_none_black54_24dp);
long mutedTill = mConversation.getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0);
if (mutedTill == Long.MAX_VALUE) {
mNotifyStatusText.setText(R.string.notify_never);
mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_off_grey600_24dp);
mNotifyStatusButton.setImageResource(ic_notifications_off);
} else if (System.currentTimeMillis() < mutedTill) {
mNotifyStatusText.setText(R.string.notify_paused);
mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_paused_grey600_24dp);
mNotifyStatusButton.setImageResource(ic_notifications_paused);
} else if (mConversation.alwaysNotify()) {
mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_grey600_24dp);
mNotifyStatusButton.setImageResource(ic_notifications);
mNotifyStatusText.setText(R.string.notify_on_all_messages);
} else {
mNotifyStatusButton.setImageResource(R.drawable.ic_notifications_none_grey600_24dp);
mNotifyStatusButton.setImageResource(ic_notifications_none);
mNotifyStatusText.setText(R.string.notify_only_when_highlighted);
}
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
membersView.removeAllViews();
final ArrayList<User> users = mucOptions.getUsers();
Collections.sort(users,new Comparator<User>() {
@Override
public int compare(User lhs, User rhs) {
return lhs.getName().compareToIgnoreCase(rhs.getName());
}
});
Collections.sort(users);
for (final User user : users) {
View view = inflater.inflate(R.layout.contact, membersView,false);
this.setListItemBackgroundOnView(view);
@ -591,16 +611,23 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
tvKey.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
}
Contact contact = user.getContact();
String name = user.getName();
if (contact != null) {
tvDisplayName.setText(contact.getDisplayName());
tvStatus.setText(user.getName() + " \u2022 " + getStatus(user));
tvStatus.setText((name != null ? name+ " \u2022 " : "") + getStatus(user));
} else {
tvDisplayName.setText(user.getName());
tvDisplayName.setText(name == null ? "" : name);
tvStatus.setText(getStatus(user));
}
ImageView iv = (ImageView) view.findViewById(R.id.contact_photo);
iv.setImageBitmap(avatarService().get(user, getPixel(48), false));
if (user.getRole() == MucOptions.Role.NONE) {
tvDisplayName.setAlpha(INACTIVE_ALPHA);
tvKey.setAlpha(INACTIVE_ALPHA);
tvStatus.setAlpha(INACTIVE_ALPHA);
iv.setAlpha(INACTIVE_ALPHA);
}
membersView.addView(view);
if (mConversation.getMucOptions().canInvite()) {
mInviteButton.setVisibility(View.VISIBLE);
@ -623,26 +650,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
}
}
@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void setListItemBackgroundOnView(View view) {
int sdk = android.os.Build.VERSION.SDK_INT;
if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
} else {
view.setBackground(getResources().getDrawable(R.drawable.greybackground));
}
}
private void viewPgpKey(User user) {
PgpEngine pgp = xmppConnectionService.getPgpEngine();
if (pgp != null) {
PendingIntent intent = pgp.getIntentForKey(
mConversation.getAccount(), user.getPgpKeyId());
PendingIntent intent = pgp.getIntentForKey(user.getPgpKeyId());
if (intent != null) {
try {
startIntentSenderForResult(intent.getIntentSender(), 0,
null, 0, 0, 0);
startIntentSenderForResult(intent.getIntentSender(), 0, null, 0, 0, 0);
} catch (SendIntentException ignored) {
}
@ -652,7 +666,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
@Override
public void onAffiliationChangedSuccessful(Jid jid) {
refreshUi();
}
@Override

View file

@ -10,11 +10,9 @@ import android.content.SharedPreferences;
import android.net.Uri;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Intents;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
@ -30,15 +28,17 @@ import android.widget.QuickContactBadge;
import android.widget.TextView;
import android.widget.Toast;
import com.wefika.flowlayout.FlowLayout;
import org.openintents.openpgp.util.OpenPgpUtils;
import java.security.cert.X509Certificate;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@ -47,13 +47,14 @@ import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.utils.XmppUri;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
public static final String ACTION_VIEW_CONTACT = "view_contact";
private Contact contact;
@ -104,17 +105,22 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
};
private Jid accountJid;
private TextView lastseen;
private Jid contactJid;
private TextView contactJidTv;
private TextView accountJidTv;
private TextView lastseen;
private TextView statusMessage;
private CheckBox send;
private CheckBox receive;
private Button addContactButton;
private Button mShowInactiveDevicesButton;
private QuickContactBadge badge;
private LinearLayout keys;
private LinearLayout tags;
private boolean showDynamicTags;
private LinearLayout keysWrapper;
private FlowLayout tags;
private boolean showDynamicTags = false;
private boolean showLastSeen = false;
private boolean showInactiveOmemo = false;
private String messageFingerprint;
private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
@ -135,22 +141,20 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
@Override
public void onClick(View v) {
if (contact.getSystemAccount() == null) {
Uri systemAccount = contact.getSystemAccount();
if (systemAccount == null) {
AlertDialog.Builder builder = new AlertDialog.Builder(
ContactDetailsActivity.this);
builder.setTitle(getString(R.string.action_add_phone_book));
builder.setMessage(getString(R.string.add_phone_book_text,
contact.getJid()));
contact.getDisplayJid()));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.add), addToPhonebook);
builder.create().show();
} else {
String[] systemAccount = contact.getSystemAccount().split("#");
long id = Long.parseLong(systemAccount[0]);
Uri uri = ContactsContract.Contacts.getLookupUri(id, systemAccount[1]);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(uri);
startActivity(intent);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(systemAccount);
startActivity(intent);
}
}
};
@ -179,7 +183,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
@Override
protected String getShareableUri() {
if (contact != null) {
return contact.getShareableUri();
return "xmpp:"+contact.getJid().toBareJid().toString();
} else {
return "";
}
@ -188,6 +192,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo",false);
if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
try {
this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT));
@ -204,6 +209,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
contactJidTv = (TextView) findViewById(R.id.details_contactjid);
accountJidTv = (TextView) findViewById(R.id.details_account);
lastseen = (TextView) findViewById(R.id.details_lastseen);
statusMessage = (TextView) findViewById(R.id.status_message);
send = (CheckBox) findViewById(R.id.details_send_presence);
receive = (CheckBox) findViewById(R.id.details_receive_presence);
badge = (QuickContactBadge) findViewById(R.id.details_contact_badge);
@ -215,14 +221,39 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
});
keys = (LinearLayout) findViewById(R.id.details_contact_keys);
tags = (LinearLayout) findViewById(R.id.tags);
keysWrapper = (LinearLayout) findViewById(R.id.keys_wrapper);
tags = (FlowLayout) findViewById(R.id.tags);
mShowInactiveDevicesButton = (Button) findViewById(R.id.show_inactive_devices);
if (getActionBar() != null) {
getActionBar().setHomeButtonEnabled(true);
getActionBar().setDisplayHomeAsUpEnabled(true);
}
mShowInactiveDevicesButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
showInactiveOmemo = !showInactiveOmemo;
populateView();
}
});
}
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false);
@Override
public void onSaveInstanceState(final Bundle savedInstanceState) {
savedInstanceState.putBoolean("show_inactive_omemo",showInactiveOmemo);
super.onSaveInstanceState(savedInstanceState);
}
@Override
public void onStart() {
super.onStart();
final int theme = findTheme();
if (this.mTheme != theme) {
recreate();
} else {
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
this.showDynamicTags = preferences.getBoolean("show_dynamic_tags", false);
this.showLastSeen = preferences.getBoolean("last_activity", false);
}
}
@Override
@ -233,17 +264,21 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
case android.R.id.home:
finish();
break;
case R.id.action_share:
shareUri();
break;
case R.id.action_delete_contact:
builder.setTitle(getString(R.string.action_delete_contact))
.setMessage(
getString(R.string.remove_contact_text,
contact.getJid()))
contact.getDisplayJid()))
.setPositiveButton(getString(R.string.delete),
removeFromRoster).create().show();
break;
case R.id.action_edit_contact:
if (contact.getSystemAccount() == null) {
quickEdit(contact.getDisplayName(), new OnValueEdited() {
Uri systemAccount = contact.getSystemAccount();
if (systemAccount == null) {
quickEdit(contact.getDisplayName(), 0, new OnValueEdited() {
@Override
public void onValueEdited(String value) {
@ -255,10 +290,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
});
} else {
Intent intent = new Intent(Intent.ACTION_EDIT);
String[] systemAccount = contact.getSystemAccount().split("#");
long id = Long.parseLong(systemAccount[0]);
Uri uri = Contacts.getLookupUri(id, systemAccount[1]);
intent.setDataAndType(uri, Contacts.CONTENT_ITEM_TYPE);
intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
intent.putExtra("finishActivityOnSaveCompleted", true);
startActivity(intent);
}
@ -298,10 +330,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
edit.setVisible(false);
delete.setVisible(false);
}
return true;
return super.onCreateOptionsMenu(menu);
}
private void populateView() {
if (contact == null) {
return;
}
invalidateOptionsMenu();
setTitle(contact.getDisplayName());
if (contact.showInRoster()) {
@ -311,6 +346,25 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
send.setOnCheckedChangeListener(null);
receive.setOnCheckedChangeListener(null);
List<String> statusMessages = contact.getPresences().getStatusMessages();
if (statusMessages.size() == 0) {
statusMessage.setVisibility(View.GONE);
} else {
StringBuilder builder = new StringBuilder();
statusMessage.setVisibility(View.VISIBLE);
int s = statusMessages.size();
for(int i = 0; i < s; ++i) {
if (s > 1) {
builder.append("");
}
builder.append(statusMessages.get(i));
if (i < s - 1) {
builder.append("\n");
}
}
statusMessage.setText(builder);
}
if (contact.getOption(Contact.Options.FROM)) {
send.setText(R.string.send_presence_updates);
send.setChecked(true);
@ -343,26 +397,32 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
receive.setEnabled(false);
send.setEnabled(false);
}
send.setOnCheckedChangeListener(this.mOnSendCheckedChange);
receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
} else {
addContactButton.setVisibility(View.VISIBLE);
send.setVisibility(View.GONE);
receive.setVisibility(View.GONE);
statusMessage.setVisibility(View.GONE);
}
if (contact.isBlocked() && !this.showDynamicTags) {
lastseen.setVisibility(View.VISIBLE);
lastseen.setText(R.string.contact_blocked);
} else {
lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.lastseen.time));
if (showLastSeen && contact.getLastseen() > 0) {
lastseen.setVisibility(View.VISIBLE);
lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
} else {
lastseen.setVisibility(View.GONE);
}
}
if (contact.getPresences().size() > 1) {
contactJidTv.setText(contact.getJid() + " ("
contactJidTv.setText(contact.getDisplayJid() + " ("
+ contact.getPresences().size() + ")");
} else {
contactJidTv.setText(contact.getJid().toString());
contactJidTv.setText(contact.getDisplayJid());
}
String account;
if (Config.DOMAIN_LOCK != null) {
@ -377,40 +437,69 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
keys.removeAllViews();
boolean hasKeys = false;
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
for(final String otrFingerprint : contact.getOtrFingerprints()) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
ImageButton removeButton = (ImageButton) view
.findViewById(R.id.button_remove);
removeButton.setVisibility(View.VISIBLE);
keyType.setText("OTR Fingerprint");
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
keys.addView(view);
removeButton.setOnClickListener(new OnClickListener() {
if (Config.supportOtr()) {
for (final String otrFingerprint : contact.getOtrFingerprints()) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
ImageButton removeButton = (ImageButton) view
.findViewById(R.id.button_remove);
removeButton.setVisibility(View.VISIBLE);
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
if (otrFingerprint != null && otrFingerprint.equals(messageFingerprint)) {
keyType.setText(R.string.otr_fingerprint_selected_message);
keyType.setTextColor(getResources().getColor(R.color.accent));
} else {
keyType.setText(R.string.otr_fingerprint);
}
keys.addView(view);
removeButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
confirmToDeleteFingerprint(otrFingerprint);
}
});
@Override
public void onClick(View v) {
confirmToDeleteFingerprint(otrFingerprint);
}
});
}
}
for (final String fingerprint : contact.getAccount().getAxolotlService().getFingerprintsForContact(contact)) {
boolean highlight = fingerprint.equals(messageFingerprint);
hasKeys |= addFingerprintRow(keys, contact.getAccount(), fingerprint, highlight, new OnClickListener() {
@Override
public void onClick(View v) {
onOmemoKeyClicked(contact.getAccount(), fingerprint);
if (Config.supportOmemo()) {
boolean skippedInactive = false;
boolean showsInactive = false;
for (final XmppAxolotlSession session : contact.getAccount().getAxolotlService().findSessionsForContact(contact)) {
final FingerprintStatus trust = session.getTrust();
if (!trust.isActive()) {
if (showInactiveOmemo) {
showsInactive = true;
} else {
skippedInactive = true;
continue;
}
}
});
if (!trust.isCompromised()) {
boolean highlight = session.getFingerprint().equals(messageFingerprint);
hasKeys = true;
addFingerprintRow(keys, session, highlight);
}
}
if (showsInactive || skippedInactive) {
mShowInactiveDevicesButton.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
mShowInactiveDevicesButton.setVisibility(View.VISIBLE);
} else {
mShowInactiveDevicesButton.setVisibility(View.GONE);
}
} else {
mShowInactiveDevicesButton.setVisibility(View.GONE);
}
if (contact.getPgpKeyId() != 0) {
if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
keyType.setText("PGP Key ID");
keyType.setText(R.string.openpgp_key_id);
if ("pgp".equals(messageFingerprint)) {
keyType.setTextColor(getResources().getColor(R.color.accent));
}
key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
view.setOnClickListener(new OnClickListener() {
@ -434,13 +523,9 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
});
keys.addView(view);
}
if (hasKeys) {
keys.setVisibility(View.VISIBLE);
} else {
keys.setVisibility(View.GONE);
}
keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
List<ListItem.Tag> tagList = contact.getTags();
List<ListItem.Tag> tagList = contact.getTags(this);
if (tagList.size() == 0 || !this.showDynamicTags) {
tags.setVisibility(View.GONE);
} else {
@ -455,40 +540,6 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
}
private void onOmemoKeyClicked(Account account, String fingerprint) {
final XmppAxolotlSession.Trust trust = account.getAxolotlService().getFingerprintTrust(fingerprint);
if (trust != null && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
X509Certificate x509Certificate = account.getAxolotlService().getFingerprintCertificate(fingerprint);
if (x509Certificate != null) {
showCertificateInformationDialog(CryptoHelper.extractCertificateInformation(x509Certificate));
} else {
Toast.makeText(this,R.string.certificate_not_found, Toast.LENGTH_SHORT).show();
}
}
}
private void showCertificateInformationDialog(Bundle bundle) {
View view = getLayoutInflater().inflate(R.layout.certificate_information, null);
final String not_available = getString(R.string.certicate_info_not_available);
TextView subject_cn = (TextView) view.findViewById(R.id.subject_cn);
TextView subject_o = (TextView) view.findViewById(R.id.subject_o);
TextView issuer_cn = (TextView) view.findViewById(R.id.issuer_cn);
TextView issuer_o = (TextView) view.findViewById(R.id.issuer_o);
TextView sha1 = (TextView) view.findViewById(R.id.sha1);
subject_cn.setText(bundle.getString("subject_cn", not_available));
subject_o.setText(bundle.getString("subject_o", not_available));
issuer_cn.setText(bundle.getString("issuer_cn", not_available));
issuer_o.setText(bundle.getString("issuer_o", not_available));
sha1.setText(bundle.getString("sha1", not_available));
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.certificate_information);
builder.setView(view);
builder.setPositiveButton(R.string.ok, null);
builder.create().show();
}
protected void confirmToDeleteFingerprint(final String fingerprint) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.delete_fingerprint);
@ -509,15 +560,17 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
builder.create().show();
}
@Override
public void onBackendConnected() {
if ((accountJid != null) && (contactJid != null)) {
Account account = xmppConnectionService
.findAccountByJid(accountJid);
if (accountJid != null && contactJid != null) {
Account account = xmppConnectionService.findAccountByJid(accountJid);
if (account == null) {
return;
}
this.contact = account.getRoster().getContact(contactJid);
if (mPendingFingerprintVerificationUri != null) {
processFingerprintVerification(mPendingFingerprintVerificationUri);
mPendingFingerprintVerificationUri = null;
}
populateView();
}
}
@ -526,4 +579,15 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
refreshUi();
}
@Override
protected void processFingerprintVerification(XmppUri uri) {
if (contact != null && contact.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
if (xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints())) {
Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
}
} else {
Toast.makeText(this,R.string.invalid_barcode,Toast.LENGTH_SHORT).show();
}
}
}

View file

@ -5,6 +5,7 @@ import android.app.ActionBar;
import android.app.AlertDialog;
import android.app.FragmentTransaction;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
@ -14,6 +15,7 @@ import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.provider.MediaStore;
import android.provider.Settings;
import android.support.v4.widget.SlidingPaneLayout;
@ -47,6 +49,7 @@ import de.timroes.android.listview.EnhancedListView;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
@ -54,6 +57,7 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
@ -61,17 +65,16 @@ import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ConversationActivity extends XmppActivity
implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast {
public static final String ACTION_DOWNLOAD = "eu.siacs.conversations.action.DOWNLOAD";
public static final String VIEW_CONVERSATION = "viewConversation";
public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
public static final String CONVERSATION = "conversationUuid";
public static final String MESSAGE = "messageUuid";
public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
public static final String TEXT = "text";
public static final String NICK = "nick";
public static final String PRIVATE_MESSAGE = "pm";
@ -91,9 +94,13 @@ public class ConversationActivity extends XmppActivity
private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
private static final String STATE_PANEL_OPEN = "state_panel_open";
private static final String STATE_PENDING_URI = "state_pending_uri";
private static final String STATE_FIRST_VISIBLE = "first_visible";
private static final String STATE_OFFSET_FROM_TOP = "offset_from_top";
private String mOpenConverstaion = null;
private String mOpenConversation = null;
private boolean mPanelOpen = true;
private AtomicBoolean mShouldPanelBeOpen = new AtomicBoolean(false);
private Pair<Integer,Integer> mScrollPosition = null;
final private List<Uri> mPendingImageUris = new ArrayList<>();
final private List<Uri> mPendingFileUris = new ArrayList<>();
private Uri mPendingGeoUri = null;
@ -115,6 +122,7 @@ public class ConversationActivity extends XmppActivity
private boolean mActivityPaused = false;
private AtomicBoolean mRedirected = new AtomicBoolean(false);
private Pair<Integer, Intent> mPostponedActivityResult;
private boolean mUnprocessedNewIntent = false;
public Conversation getSelectedConversation() {
return this.mSelectedConversation;
@ -127,6 +135,7 @@ public class ConversationActivity extends XmppActivity
public void showConversationsOverview() {
if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
mShouldPanelBeOpen.set(true);
mSlidingPaneLayout.openPane();
}
}
@ -144,22 +153,18 @@ public class ConversationActivity extends XmppActivity
public void hideConversationsOverview() {
if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
mShouldPanelBeOpen.set(false);
mSlidingPaneLayout.closePane();
}
}
public boolean isConversationsOverviewHideable() {
if (mContentView instanceof SlidingPaneLayout) {
return true;
} else {
return false;
}
return mContentView instanceof SlidingPaneLayout;
}
public boolean isConversationsOverviewVisable() {
if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
return mSlidingPaneLayout.isOpen();
return mShouldPanelBeOpen.get();
} else {
return true;
}
@ -169,10 +174,19 @@ public class ConversationActivity extends XmppActivity
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
mOpenConverstaion = savedInstanceState.getString(STATE_OPEN_CONVERSATION, null);
mOpenConversation = savedInstanceState.getString(STATE_OPEN_CONVERSATION, null);
mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true);
int pos = savedInstanceState.getInt(STATE_FIRST_VISIBLE, -1);
int offset = savedInstanceState.getInt(STATE_OFFSET_FROM_TOP, 1);
if (pos >= 0 && offset <= 0) {
Log.d(Config.LOGTAG,"retrieved scroll position from instanceState "+pos+":"+offset);
mScrollPosition = new Pair<>(pos,offset);
} else {
mScrollPosition = null;
}
String pending = savedInstanceState.getString(STATE_PENDING_URI, null);
if (pending != null) {
Log.d(Config.LOGTAG,"ConversationsActivity.onCreate() - restoring pending image uri");
mPendingImageUris.clear();
mPendingImageUris.add(Uri.parse(pending));
}
@ -284,26 +298,25 @@ public class ConversationActivity extends XmppActivity
}
if (mContentView instanceof SlidingPaneLayout) {
SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView;
mSlidingPaneLayout.setParallaxDistance(150);
mSlidingPaneLayout
.setShadowResource(R.drawable.es_slidingpane_shadow);
mSlidingPaneLayout.setShadowResource(R.drawable.es_slidingpane_shadow);
mSlidingPaneLayout.setSliderFadeColor(0);
mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() {
@Override
public void onPanelOpened(View arg0) {
mShouldPanelBeOpen.set(true);
updateActionBarTitle();
invalidateOptionsMenu();
hideKeyboard();
if (xmppConnectionServiceBound) {
xmppConnectionService.getNotificationService()
.setOpenConversation(null);
xmppConnectionService.getNotificationService().setOpenConversation(null);
}
closeContextMenu();
}
@Override
public void onPanelClosed(View arg0) {
mShouldPanelBeOpen.set(false);
listView.discardUndo();
openConversation();
}
@ -365,12 +378,8 @@ public class ConversationActivity extends XmppActivity
}
public void sendReadMarkerIfNecessary(final Conversation conversation) {
if (!mActivityPaused && conversation != null) {
if (!conversation.isRead()) {
xmppConnectionService.sendReadMarker(conversation);
} else {
xmppConnectionService.markRead(conversation);
}
if (!mActivityPaused && !mUnprocessedNewIntent && conversation != null) {
xmppConnectionService.sendReadMarker(conversation);
}
}
@ -412,9 +421,11 @@ public class ConversationActivity extends XmppActivity
menuContactDetails.setVisible(false);
menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable() && getSelectedConversation().getMucOptions().participating());
menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
menuSecure.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); //if pgp is hidden conferences have no choice of encryption
menuSecure.setVisible((Config.supportOpenPgp() || Config.supportOmemo()) && Config.multipleEncryptionChoices()); //only if pgp is supported we have a choice
} else {
menuContactDetails.setVisible(!this.getSelectedConversation().withSelf());
menuMucDetails.setVisible(false);
menuSecure.setVisible(Config.multipleEncryptionChoices());
}
if (this.getSelectedConversation().isMuted()) {
menuMute.setVisible(false);
@ -423,7 +434,46 @@ public class ConversationActivity extends XmppActivity
}
}
}
return true;
if (Config.supportOmemo()) {
new Handler().post(new Runnable() {
@Override
public void run() {
View view = findViewById(R.id.action_security);
if (view != null) {
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
return quickOmemoDebugger(getSelectedConversation());
}
});
}
}
});
}
return super.onCreateOptionsMenu(menu);
}
private boolean quickOmemoDebugger(Conversation c) {
if (c != null) {
boolean single = c.getMode() == Conversation.MODE_SINGLE;
AxolotlService axolotlService = c.getAccount().getAxolotlService();
Pair<AxolotlService.AxolotlCapability,Jid> capabilityJidPair = axolotlService.isConversationAxolotlCapableDetailed(c);
switch (capabilityJidPair.first) {
case MISSING_PRESENCE:
Toast.makeText(ConversationActivity.this,single ? getString(R.string.missing_presence_subscription) : getString(R.string.missing_presence_subscription_with_x,capabilityJidPair.second.toBareJid().toString()),Toast.LENGTH_SHORT).show();
return true;
case MISSING_KEYS:
Toast.makeText(ConversationActivity.this,single ? getString(R.string.missing_omemo_keys) : getString(R.string.missing_keys_from_x,capabilityJidPair.second.toBareJid().toString()),Toast.LENGTH_SHORT).show();
return true;
case WRONG_CONFIGURATION:
Toast.makeText(ConversationActivity.this,R.string.wrong_conference_configuration, Toast.LENGTH_SHORT).show();
return true;
case NO_MEMBERS:
Toast.makeText(ConversationActivity.this,R.string.this_conference_has_no_members, Toast.LENGTH_SHORT).show();
return true;
}
}
return false;
}
protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
@ -447,6 +497,8 @@ public class ConversationActivity extends XmppActivity
break;
case ATTACHMENT_CHOICE_TAKE_PHOTO:
Uri uri = xmppConnectionService.getFileBackend().getTakePhotoUri();
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
mPendingImageUris.clear();
@ -501,7 +553,7 @@ public class ConversationActivity extends XmppActivity
public void attachFile(final int attachmentChoice) {
if (attachmentChoice != ATTACHMENT_CHOICE_LOCATION) {
if (!hasStoragePermission(attachmentChoice)) {
if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(attachmentChoice)) {
return;
}
}
@ -541,7 +593,7 @@ public class ConversationActivity extends XmppActivity
@Override
public void error(int error, Contact contact) {
displayErrorDialog(error);
replaceToast(getString(error));
}
});
} else if (mode == Conversation.MODE_MULTI && conversation.getMucOptions().pgpKeysInUse()) {
@ -564,10 +616,8 @@ public class ConversationActivity extends XmppActivity
@Override
public void onClick(DialogInterface dialog,
int which) {
conversation
.setNextEncryption(Message.ENCRYPTION_NONE);
xmppConnectionService.databaseBackend
.updateConversation(conversation);
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
xmppConnectionService.updateConversation(conversation);
selectPresenceToAttachFile(attachmentChoice, Message.ENCRYPTION_NONE);
}
});
@ -600,7 +650,7 @@ public class ConversationActivity extends XmppActivity
}
public void startDownloadable(Message message) {
if (!hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
if (!Config.ONLY_INTERNAL_STORAGE && !hasStoragePermission(ConversationActivity.REQUEST_START_DOWNLOAD)) {
this.mPendingDownloadableMessage = message;
return;
}
@ -825,7 +875,7 @@ public class ConversationActivity extends XmppActivity
conversation.setNextEncryption(Message.ENCRYPTION_PGP);
item.setChecked(true);
} else {
announcePgp(conversation.getAccount(), conversation);
announcePgp(conversation.getAccount(), conversation, onOpenPGPKeyPublished);
}
} else {
showInstallPgpDialog();
@ -841,7 +891,7 @@ public class ConversationActivity extends XmppActivity
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
break;
}
xmppConnectionService.databaseBackend.updateConversation(conversation);
xmppConnectionService.updateConversation(conversation);
fragment.updateChatMsgHint();
invalidateOptionsMenu();
refreshUi();
@ -853,13 +903,14 @@ public class ConversationActivity extends XmppActivity
MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none);
MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp);
MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl);
pgp.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION);
none.setVisible(!Config.FORCE_E2E_ENCRYPTION);
otr.setVisible(!Config.X509_VERIFICATION);
pgp.setVisible(Config.supportOpenPgp());
none.setVisible(Config.supportUnencrypted() || conversation.getMode() == Conversation.MODE_MULTI);
otr.setVisible(Config.supportOtr());
axolotl.setVisible(Config.supportOmemo());
if (conversation.getMode() == Conversation.MODE_MULTI) {
otr.setVisible(false);
axolotl.setVisible(false);
} else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
}
if (!conversation.getAccount().getAxolotlService().isConversationAxolotlCapable(conversation)) {
axolotl.setEnabled(false);
}
switch (conversation.getNextEncryption()) {
@ -899,8 +950,7 @@ public class ConversationActivity extends XmppActivity
till = System.currentTimeMillis() + (durations[which] * 1000);
}
conversation.setMutedTill(till);
ConversationActivity.this.xmppConnectionService.databaseBackend
.updateConversation(conversation);
ConversationActivity.this.xmppConnectionService.updateConversation(conversation);
updateConversationList();
ConversationActivity.this.mConversationFragment.updateMessages();
invalidateOptionsMenu();
@ -911,7 +961,7 @@ public class ConversationActivity extends XmppActivity
public void unmuteConversation(final Conversation conversation) {
conversation.setMutedTill(0);
this.xmppConnectionService.databaseBackend.updateConversation(conversation);
this.xmppConnectionService.updateConversation(conversation);
updateConversationList();
ConversationActivity.this.mConversationFragment.updateMessages();
invalidateOptionsMenu();
@ -922,7 +972,7 @@ public class ConversationActivity extends XmppActivity
if (!isConversationsOverviewVisable()) {
showConversationsOverview();
} else {
moveTaskToBack(true);
super.onBackPressed();
}
}
@ -944,14 +994,18 @@ public class ConversationActivity extends XmppActivity
upKey = KeyEvent.KEYCODE_DPAD_RIGHT;
downKey = KeyEvent.KEYCODE_DPAD_LEFT;
break;
case Surface.ROTATION_0:
default:
upKey = KeyEvent.KEYCODE_DPAD_UP;
downKey = KeyEvent.KEYCODE_DPAD_DOWN;
}
final boolean modifier = event.isCtrlPressed() || event.isAltPressed();
final boolean modifier = event.isCtrlPressed() || (event.getMetaState() & KeyEvent.META_ALT_LEFT_ON) != 0;
if (modifier && key == KeyEvent.KEYCODE_TAB && isConversationsOverviewHideable()) {
toggleConversationsOverview();
return true;
} else if (modifier && key == KeyEvent.KEYCODE_SPACE) {
startActivity(new Intent(this, StartConversationActivity.class));
return true;
} else if (modifier && key == downKey) {
if (isConversationsOverviewHideable() && !isConversationsOverviewVisable()) {
showConversationsOverview();
@ -1036,13 +1090,15 @@ public class ConversationActivity extends XmppActivity
@Override
protected void onNewIntent(final Intent intent) {
if (xmppConnectionServiceBound) {
if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) {
if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
mOpenConversation = null;
mUnprocessedNewIntent = true;
if (xmppConnectionServiceBound) {
handleViewConversationIntent(intent);
setIntent(new Intent());
intent.setAction(Intent.ACTION_MAIN);
} else {
setIntent(intent);
}
} else {
setIntent(intent);
}
}
@ -1063,9 +1119,6 @@ public class ConversationActivity extends XmppActivity
listView.discardUndo();
super.onPause();
this.mActivityPaused = true;
if (this.xmppConnectionServiceBound) {
this.xmppConnectionService.getNotificationService().setIsInForeground(false);
}
}
@Override
@ -1077,9 +1130,7 @@ public class ConversationActivity extends XmppActivity
recreate();
}
this.mActivityPaused = false;
if (this.xmppConnectionServiceBound) {
this.xmppConnectionService.getNotificationService().setIsInForeground(true);
}
if (!isConversationsOverviewVisable() || !isConversationsOverviewHideable()) {
sendReadMarkerIfNecessary(getSelectedConversation());
@ -1092,11 +1143,17 @@ public class ConversationActivity extends XmppActivity
Conversation conversation = getSelectedConversation();
if (conversation != null) {
savedInstanceState.putString(STATE_OPEN_CONVERSATION, conversation.getUuid());
Pair<Integer,Integer> scrollPosition = mConversationFragment.getScrollPosition();
if (scrollPosition != null) {
savedInstanceState.putInt(STATE_FIRST_VISIBLE, scrollPosition.first);
savedInstanceState.putInt(STATE_OFFSET_FROM_TOP, scrollPosition.second);
}
} else {
savedInstanceState.remove(STATE_OPEN_CONVERSATION);
}
savedInstanceState.putBoolean(STATE_PANEL_OPEN, isConversationsOverviewVisable());
if (this.mPendingImageUris.size() >= 1) {
Log.d(Config.LOGTAG,"ConversationsActivity.onSaveInstanceState() - saving pending image uri");
savedInstanceState.putString(STATE_PENDING_URI, this.mPendingImageUris.get(0).toString());
} else {
savedInstanceState.remove(STATE_PENDING_URI);
@ -1117,30 +1174,41 @@ public class ConversationActivity extends XmppActivity
updateConversationList();
if (mPendingConferenceInvite != null) {
mPendingConferenceInvite.execute(this);
if (mPendingConferenceInvite.execute(this)) {
mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
mToast.show();
}
mPendingConferenceInvite = null;
}
final Intent intent = getIntent();
if (xmppConnectionService.getAccounts().size() == 0) {
if (mRedirected.compareAndSet(false, true)) {
if (Config.X509_VERIFICATION) {
startActivity(new Intent(this, ManageAccountActivity.class));
} else if (Config.MAGIC_CREATE_DOMAIN != null) {
startActivity(new Intent(this, WelcomeActivity.class));
} else {
startActivity(new Intent(this, EditAccountActivity.class));
Intent editAccount = new Intent(this, EditAccountActivity.class);
editAccount.putExtra("init",true);
startActivity(editAccount);
}
finish();
}
} else if (conversationList.size() <= 0) {
if (mRedirected.compareAndSet(false, true)) {
Intent intent = new Intent(this, StartConversationActivity.class);
intent.putExtra("init", true);
startActivity(intent);
Account pendingAccount = xmppConnectionService.getPendingAccount();
if (pendingAccount == null) {
Intent startConversationActivity = new Intent(this, StartConversationActivity.class);
intent.putExtra("init", true);
startActivity(startConversationActivity);
} else {
switchToAccount(pendingAccount, true);
}
finish();
}
} else if (getIntent() != null && VIEW_CONVERSATION.equals(getIntent().getType())) {
clearPending();
handleViewConversationIntent(getIntent());
} else if (selectConversationByUuid(mOpenConverstaion)) {
} else if (selectConversationByUuid(mOpenConversation)) {
if (mPanelOpen) {
showConversationsOverview();
} else {
@ -1149,8 +1217,15 @@ public class ConversationActivity extends XmppActivity
updateActionBarTitle(true);
}
}
this.mConversationFragment.reInit(getSelectedConversation());
mOpenConverstaion = null;
if (this.mConversationFragment.reInit(getSelectedConversation())) {
Log.d(Config.LOGTAG,"setting scroll position on fragment");
this.mConversationFragment.setScrollPosition(mScrollPosition);
}
mOpenConversation = null;
} else if (intent != null && ACTION_VIEW_CONVERSATION.equals(intent.getAction())) {
clearPending();
handleViewConversationIntent(intent);
intent.setAction(Intent.ACTION_MAIN);
} else if (getSelectedConversation() == null) {
showConversationsOverview();
clearPending();
@ -1166,13 +1241,22 @@ public class ConversationActivity extends XmppActivity
this.onActivityResult(mPostponedActivityResult.first, RESULT_OK, mPostponedActivityResult.second);
}
final boolean stopping;
if (Build.VERSION.SDK_INT >= 17) {
stopping = isFinishing() || isDestroyed();
} else {
stopping = isFinishing();
}
if (!forbidProcessingPendings) {
for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
Uri foo = i.next();
Log.d(Config.LOGTAG,"ConversationsActivity.onBackendConnected() - attaching image to conversations. stopping="+Boolean.toString(stopping));
attachImageToConversation(getSelectedConversation(), foo);
}
for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
Log.d(Config.LOGTAG,"ConversationsActivity.onBackendConnected() - attaching file to conversations. stopping="+Boolean.toString(stopping));
attachFileToConversation(getSelectedConversation(), i.next());
}
@ -1186,12 +1270,16 @@ public class ConversationActivity extends XmppActivity
if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) {
openBatteryOptimizationDialogIfNeeded();
}
setIntent(new Intent());
if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) {
xmppConnectionService.getNotificationService().setOpenConversation(null);
} else {
xmppConnectionService.getNotificationService().setOpenConversation(getSelectedConversation());
}
}
private void handleViewConversationIntent(final Intent intent) {
final String uuid = intent.getStringExtra(CONVERSATION);
final String downloadUuid = intent.getStringExtra(MESSAGE);
final String downloadUuid = intent.getStringExtra(EXTRA_DOWNLOAD_UUID);
final String text = intent.getStringExtra(TEXT);
final String nick = intent.getStringExtra(NICK);
final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE, false);
@ -1213,6 +1301,7 @@ public class ConversationActivity extends XmppActivity
this.mConversationFragment.appendText(text);
}
hideConversationsOverview();
mUnprocessedNewIntent = false;
openConversation();
if (mContentView instanceof SlidingPaneLayout) {
updateActionBarTitle(true); //fixes bug where slp isn't properly closed yet
@ -1223,6 +1312,8 @@ public class ConversationActivity extends XmppActivity
startDownloadable(message);
}
}
} else {
mUnprocessedNewIntent = false;
}
}
@ -1253,9 +1344,11 @@ public class ConversationActivity extends XmppActivity
}
Uri uri = intent.getData();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && uri == null) {
ClipData clipData = intent.getClipData();
for (int i = 0; i < clipData.getItemCount(); ++i) {
uris.add(clipData.getItemAt(i).getUri());
final ClipData clipData = intent.getClipData();
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); ++i) {
uris.add(clipData.getItemAt(i).getUri());
}
}
} else {
uris.add(uri);
@ -1276,7 +1369,7 @@ public class ConversationActivity extends XmppActivity
// associate selected PGP keyId with the account
mSelectedConversation.getAccount().setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
// we need to announce the key as described in XEP-027
announcePgp(mSelectedConversation.getAccount(), null);
announcePgp(mSelectedConversation.getAccount(), null, onOpenPGPKeyPublished);
} else {
choosePgpSignId(mSelectedConversation.getAccount());
}
@ -1286,7 +1379,7 @@ public class ConversationActivity extends XmppActivity
}
} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
if (xmppConnectionServiceBound) {
announcePgp(mSelectedConversation.getAccount(), mSelectedConversation);
announcePgp(mSelectedConversation.getAccount(), mSelectedConversation, onOpenPGPKeyPublished);
this.mPostponedActivityResult = null;
} else {
this.mPostponedActivityResult = new Pair<>(requestCode, data);
@ -1296,27 +1389,47 @@ public class ConversationActivity extends XmppActivity
mPendingImageUris.addAll(extractUriFromIntent(data));
if (xmppConnectionServiceBound) {
for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching image to conversations. CHOOSE_IMAGE");
attachImageToConversation(getSelectedConversation(), i.next());
}
}
} else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE) {
mPendingFileUris.clear();
mPendingFileUris.addAll(extractUriFromIntent(data));
if (xmppConnectionServiceBound) {
for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
attachFileToConversation(getSelectedConversation(), i.next());
final List<Uri> uris = extractUriFromIntent(data);
final Conversation c = getSelectedConversation();
final OnPresenceSelected callback = new OnPresenceSelected() {
@Override
public void onPresenceSelected() {
mPendingFileUris.clear();
mPendingFileUris.addAll(uris);
if (xmppConnectionServiceBound) {
for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching file to conversations. CHOOSE_FILE/RECORD_VOICE");
attachFileToConversation(c, i.next());
}
}
}
};
if (c == null || c.getMode() == Conversation.MODE_MULTI
|| FileBackend.allFilesUnderSize(this, uris, getMaxHttpUploadSize(c))
|| c.getNextEncryption() == Message.ENCRYPTION_OTR) {
callback.onPresenceSelected();
} else {
selectPresence(c, callback);
}
} else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) {
if (mPendingImageUris.size() == 1) {
Uri uri = mPendingImageUris.get(0);
Uri uri = FileBackend.getIndexableTakePhotoUri(mPendingImageUris.get(0));
mPendingImageUris.set(0, uri);
if (xmppConnectionServiceBound) {
Log.d(Config.LOGTAG,"ConversationsActivity.onActivityResult() - attaching image to conversations. TAKE_PHOTO");
attachImageToConversation(getSelectedConversation(), uri);
mPendingImageUris.clear();
}
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(uri);
sendBroadcast(intent);
if (!Config.ONLY_INTERNAL_STORAGE) {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(uri);
sendBroadcast(intent);
}
} else {
mPendingImageUris.clear();
}
@ -1350,12 +1463,19 @@ public class ConversationActivity extends XmppActivity
}
}
private long getMaxHttpUploadSize(Conversation conversation) {
final XmppConnection connection = conversation.getAccount().getXmppConnection();
return connection == null ? -1 : connection.getFeatures().getMaxHttpUploadSize();
}
private void setNeverAskForBatteryOptimizationsAgain() {
getPreferences().edit().putBoolean("show_battery_optimization", false).commit();
}
private void openBatteryOptimizationDialogIfNeeded() {
if (showBatteryOptimizationWarning() && getPreferences().getBoolean("show_battery_optimization", true)) {
if (hasAccountWithoutPush()
&& isOptimizingBattery()
&& getPreferences().getBoolean("show_battery_optimization", true)) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.battery_optimizations_enabled);
builder.setMessage(R.string.battery_optimizations_enabled_dialog);
@ -1365,7 +1485,11 @@ public class ConversationActivity extends XmppActivity
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
Uri uri = Uri.parse("package:" + getPackageName());
intent.setData(uri);
startActivityForResult(intent, REQUEST_BATTERY_OP);
try {
startActivityForResult(intent, REQUEST_BATTERY_OP);
} catch (ActivityNotFoundException e) {
Toast.makeText(ConversationActivity.this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
}
}
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
@ -1380,6 +1504,16 @@ public class ConversationActivity extends XmppActivity
}
}
private boolean hasAccountWithoutPush() {
for(Account account : xmppConnectionService.getAccounts()) {
if (account.getStatus() != Account.State.DISABLED
&& !xmppConnectionService.getPushManagementService().availableAndUseful(account)) {
return true;
}
}
return false;
}
private void attachLocationToConversation(Conversation conversation, Uri uri) {
if (conversation == null) {
return;
@ -1409,26 +1543,53 @@ public class ConversationActivity extends XmppActivity
}
final Toast prepareFileToast = Toast.makeText(getApplicationContext(),getText(R.string.preparing_file), Toast.LENGTH_LONG);
prepareFileToast.show();
xmppConnectionService.attachFileToConversation(conversation, uri, new UiCallback<Message>() {
xmppConnectionService.attachFileToConversation(conversation, uri, new UiInformableCallback<Message>() {
@Override
public void inform(final String text) {
hidePrepareFileToast(prepareFileToast);
runOnUiThread(new Runnable() {
@Override
public void run() {
replaceToast(text);
}
});
}
@Override
public void success(Message message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
hideToast();
}
});
hidePrepareFileToast(prepareFileToast);
xmppConnectionService.sendMessage(message);
}
@Override
public void error(int errorCode, Message message) {
public void error(final int errorCode, Message message) {
hidePrepareFileToast(prepareFileToast);
displayErrorDialog(errorCode);
runOnUiThread(new Runnable() {
@Override
public void run() {
replaceToast(getString(errorCode));
}
});
}
@Override
public void userInputRequried(PendingIntent pi, Message message) {
hidePrepareFileToast(prepareFileToast);
}
});
}
public void attachImageToConversation(Uri uri) {
this.attachImageToConversation(getSelectedConversation(), uri);
}
private void attachImageToConversation(Conversation conversation, Uri uri) {
if (conversation == null) {
return;
@ -1450,9 +1611,14 @@ public class ConversationActivity extends XmppActivity
}
@Override
public void error(int error, Message message) {
public void error(final int error, Message message) {
hidePrepareFileToast(prepareFileToast);
displayErrorDialog(error);
runOnUiThread(new Runnable() {
@Override
public void run() {
replaceToast(getString(error));
}
});
}
});
}
@ -1495,21 +1661,30 @@ public class ConversationActivity extends XmppActivity
new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi,
Message message) {
ConversationActivity.this.runIntent(pi,
ConversationActivity.REQUEST_SEND_MESSAGE);
public void userInputRequried(PendingIntent pi,Message message) {
ConversationActivity.this.runIntent(pi,ConversationActivity.REQUEST_SEND_MESSAGE);
}
@Override
public void success(Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
xmppConnectionService.sendMessage(message);
if (mConversationFragment != null) {
mConversationFragment.messageSent();
}
}
@Override
public void error(int error, Message message) {
public void error(final int error, Message message) {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ConversationActivity.this,
R.string.unable_to_connect_to_keychain,
Toast.LENGTH_SHORT
).show();
}
});
}
});
}
@ -1522,8 +1697,8 @@ public class ConversationActivity extends XmppActivity
return getPreferences().getBoolean("indicate_received", false);
}
public boolean useWhiteBackground() {
return getPreferences().getBoolean("use_white_background",false);
public boolean useGreenBackground() {
return getPreferences().getBoolean("use_green_background",true);
}
protected boolean trustKeysIfNeeded(int requestCode) {
@ -1532,18 +1707,23 @@ public class ConversationActivity extends XmppActivity
protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
Contact contact = mSelectedConversation.getContact();
boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED).isEmpty();
boolean hasUndecidedContact = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED,contact).isEmpty();
final List<Jid> targets = axolotlService.getCryptoTargets(mSelectedConversation);
boolean hasUnaccepted = !mSelectedConversation.getAcceptedCryptoTargets().containsAll(targets);
boolean hasUndecidedOwn = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided()).isEmpty();
boolean hasUndecidedContacts = !axolotlService.getKeysWithTrust(FingerprintStatus.createActiveUndecided(), targets).isEmpty();
boolean hasPendingKeys = !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0;
if(hasUndecidedOwn || hasUndecidedContact || hasPendingKeys || hasNoTrustedKeys) {
boolean hasNoTrustedKeys = axolotlService.anyTargetHasNoTrustedKeys(targets);
if(hasUndecidedOwn || hasUndecidedContacts || hasPendingKeys || hasNoTrustedKeys || hasUnaccepted) {
axolotlService.createSessionsIfNeeded(mSelectedConversation);
Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString());
String[] contacts = new String[targets.size()];
for(int i = 0; i < contacts.length; ++i) {
contacts[i] = targets.get(i).toString();
}
intent.putExtra("contacts", contacts);
intent.putExtra(EXTRA_ACCOUNT, mSelectedConversation.getAccount().getJid().toBareJid().toString());
intent.putExtra("choice", attachmentChoice);
intent.putExtra("has_no_trusted", hasNoTrustedKeys);
intent.putExtra("conversation",mSelectedConversation.getUuid());
startActivityForResult(intent, requestCode);
return true;
} else {
@ -1555,9 +1735,14 @@ public class ConversationActivity extends XmppActivity
protected void refreshUiReal() {
updateConversationList();
if (conversationList.size() > 0) {
if (!this.mConversationFragment.isAdded()) {
Log.d(Config.LOGTAG,"fragment NOT added to activity. detached="+Boolean.toString(mConversationFragment.isDetached()));
}
ConversationActivity.this.mConversationFragment.updateMessages();
updateActionBarTitle();
invalidateOptionsMenu();
} else {
Log.d(Config.LOGTAG,"not updating conversations fragment because conversations list size was 0");
}
}

View file

@ -1,15 +1,33 @@
package eu.siacs.conversations.ui;
import android.support.v13.view.inputmethod.EditorInfoCompat;
import android.support.v13.view.inputmethod.InputConnectionCompat;
import android.support.v13.view.inputmethod.InputContentInfoCompat;
import android.content.Context;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Spanned;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
import eu.siacs.conversations.Config;
public class EditMessage extends EditText {
public interface OnCommitContentListener {
boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] mimeTypes);
}
private OnCommitContentListener mCommitContentListener = null;
private String[] mimeTypes = null;
public EditMessage(Context context, AttributeSet attrs) {
super(context, attrs);
}
@ -69,6 +87,7 @@ public class EditMessage extends EditText {
this.isUserTyping = false;
this.keyboardListener.onTextDeleted();
}
this.keyboardListener.onTextChanged();
}
}
@ -84,7 +103,63 @@ public class EditMessage extends EditText {
void onTypingStarted();
void onTypingStopped();
void onTextDeleted();
void onTextChanged();
boolean onTabPressed(boolean repeated);
}
private static final InputFilter SPAN_FILTER = new InputFilter() {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
return source instanceof Spanned ? source.toString() : source;
}
};
@Override
public boolean onTextContextMenuItem(int id) {
if (id == android.R.id.paste) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return super.onTextContextMenuItem(android.R.id.pasteAsPlainText);
} else {
Editable editable = getEditableText();
InputFilter[] filters = editable.getFilters();
InputFilter[] tempFilters = new InputFilter[filters != null ? filters.length + 1 : 1];
if (filters != null) {
System.arraycopy(filters, 0, tempFilters, 1, filters.length);
}
tempFilters[0] = SPAN_FILTER;
editable.setFilters(tempFilters);
try {
return super.onTextContextMenuItem(id);
} finally {
editable.setFilters(filters);
}
}
} else {
return super.onTextContextMenuItem(id);
}
}
public void setRichContentListener(String[] mimeTypes, OnCommitContentListener listener) {
this.mimeTypes = mimeTypes;
this.mCommitContentListener = listener;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
final InputConnection ic = super.onCreateInputConnection(editorInfo);
if (mimeTypes != null && mCommitContentListener != null) {
EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes);
return InputConnectionCompat.createWrapper(ic, editorInfo, new InputConnectionCompat.OnCommitContentListener() {
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
return EditMessage.this.mCommitContentListener.onCommitContent(inputContentInfo, flags, opts, mimeTypes);
}
});
}
else {
return ic;
}
}
}

View file

@ -1,14 +1,14 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import android.widget.Spinner;
import android.widget.TextView;
import java.util.List;
@ -47,9 +47,11 @@ public class EnterJidDialog {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
builder.setTitle(title);
View dialogView = LayoutInflater.from(context).inflate(R.layout.enter_jid_dialog, null);
final TextView jabberIdDesc = (TextView) dialogView.findViewById(R.id.jabber_id);
jabberIdDesc.setText(R.string.account_settings_jabber_id);
final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account);
final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid);
jid.setAdapter(new KnownHostsAdapter(context,android.R.layout.simple_list_item_1, knownHosts));
jid.setAdapter(new KnownHostsAdapter(context, R.layout.simple_list_item, knownHosts));
if (prefilledJid != null) {
jid.append(prefilledJid);
if (!allowEditJid) {
@ -60,15 +62,16 @@ public class EnterJidDialog {
}
}
jid.setHint(R.string.account_settings_example_jabber_id);
if (account == null) {
StartConversationActivity.populateAccountSpinner(context, activatedAccounts, spinner);
} else {
ArrayAdapter<String> adapter = new ArrayAdapter<>(context,
android.R.layout.simple_spinner_item,
R.layout.simple_list_item,
new String[] { account });
spinner.setEnabled(false);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
adapter.setDropDownViewResource(R.layout.simple_list_item);
spinner.setAdapter(adapter);
}
@ -118,8 +121,9 @@ public class EnterJidDialog {
this.listener = listener;
}
public void show() {
public Dialog show() {
this.dialog.show();
this.dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this.dialogOnClick);
return this.dialog;
}
}

View file

@ -1,29 +0,0 @@
package eu.siacs.conversations.ui;
import android.content.Context;
import android.content.Intent;
import android.preference.Preference;
import android.util.AttributeSet;
import eu.siacs.conversations.services.ExportLogsService;
public class ExportLogsPreference extends Preference {
public ExportLogsPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ExportLogsPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExportLogsPreference(Context context) {
super(context);
}
protected void onClick() {
final Intent startIntent = new Intent(getContext(), ExportLogsService.class);
getContext().startService(startIntent);
super.onClick();
}
}

View file

@ -0,0 +1,120 @@
package eu.siacs.conversations.ui;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import java.security.SecureRandom;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class MagicCreateActivity extends XmppActivity implements TextWatcher {
private TextView mFullJidDisplay;
private EditText mUsername;
private SecureRandom mRandom;
private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456780+-/#$!?";
private static final int PW_LENGTH = 10;
@Override
protected void refreshUiReal() {
}
@Override
void onBackendConnected() {
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
if (getResources().getBoolean(R.bool.portrait_only)) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.magic_create);
mFullJidDisplay = (TextView) findViewById(R.id.full_jid);
mUsername = (EditText) findViewById(R.id.username);
mRandom = new SecureRandom();
Button next = (Button) findViewById(R.id.create_account);
next.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
String username = mUsername.getText().toString();
if (username.contains("@") || username.length() < 3) {
mUsername.setError(getString(R.string.invalid_username));
mUsername.requestFocus();
} else {
mUsername.setError(null);
try {
Jid jid = Jid.fromParts(username.toLowerCase(), Config.MAGIC_CREATE_DOMAIN, null);
Account account = xmppConnectionService.findAccountByJid(jid);
if (account == null) {
account = new Account(jid, createPassword());
account.setOption(Account.OPTION_REGISTER, true);
account.setOption(Account.OPTION_DISABLED, true);
account.setOption(Account.OPTION_MAGIC_CREATE, true);
xmppConnectionService.createAccount(account);
}
Intent intent = new Intent(MagicCreateActivity.this, EditAccountActivity.class);
intent.putExtra("jid", account.getJid().toBareJid().toString());
intent.putExtra("init", true);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
Toast.makeText(MagicCreateActivity.this, R.string.secure_password_generated, Toast.LENGTH_SHORT).show();
startActivity(intent);
} catch (InvalidJidException e) {
mUsername.setError(getString(R.string.invalid_username));
mUsername.requestFocus();
}
}
}
});
mUsername.addTextChangedListener(this);
}
private String createPassword() {
StringBuilder builder = new StringBuilder(PW_LENGTH);
for(int i = 0; i < PW_LENGTH; ++i) {
builder.append(CHARS.charAt(mRandom.nextInt(CHARS.length() - 1)));
}
return builder.toString();
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (s.toString().trim().length() > 0) {
try {
mFullJidDisplay.setVisibility(View.VISIBLE);
Jid jid = Jid.fromParts(s.toString().toLowerCase(), Config.MAGIC_CREATE_DOMAIN, null);
mFullJidDisplay.setText(getString(R.string.your_full_jid_will_be, jid.toString()));
} catch (InvalidJidException e) {
mFullJidDisplay.setVisibility(View.INVISIBLE);
}
} else {
mFullJidDisplay.setVisibility(View.INVISIBLE);
}
}
}

View file

@ -102,6 +102,15 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
registerForContextMenu(accountListView);
}
@Override
protected void onStart() {
super.onStart();
final int theme = findTheme();
if (this.mTheme != theme) {
recreate();
}
}
@Override
public void onSaveInstanceState(final Bundle savedInstanceState) {
if (selectedAccount != null) {
@ -121,9 +130,11 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
menu.findItem(R.id.mgmt_account_disable).setVisible(false);
menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(false);
menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
menu.findItem(R.id.mgmt_account_change_presence).setVisible(false);
} else {
menu.findItem(R.id.mgmt_account_enable).setVisible(false);
menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(!Config.HIDE_PGP_IN_UI);
menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(Config.supportOpenPgp());
menu.findItem(R.id.mgmt_account_change_presence).setVisible(manuallyChangePresence());
}
menu.setHeaderTitle(this.selectedAccount.getJid().toBareJid().toString());
}
@ -184,6 +195,9 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
case R.id.mgmt_account_announce_pgp:
publishOpenPGPPublicKey(selectedAccount);
return true;
case R.id.mgmt_account_change_presence:
changePresence(selectedAccount);
return true;
default:
return super.onContextItemSelected(item);
}
@ -232,6 +246,12 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
}
}
private void changePresence(Account account) {
Intent intent = new Intent(this, SetPresenceActivity.class);
intent.putExtra(SetPresenceActivity.EXTRA_ACCOUNT,account.getJid().toBareJid().toString());
startActivity(intent);
}
public void onClickTglAccountState(Account account, boolean enable) {
if (enable) {
enableAccount(account);
@ -307,17 +327,21 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
private void disableAccount(Account account) {
account.setOption(Account.OPTION_DISABLED, true);
xmppConnectionService.updateAccount(account);
if (!xmppConnectionService.updateAccount(account)) {
Toast.makeText(this,R.string.unable_to_update_account,Toast.LENGTH_SHORT).show();
}
}
private void enableAccount(Account account) {
account.setOption(Account.OPTION_DISABLED, false);
xmppConnectionService.updateAccount(account);
if (!xmppConnectionService.updateAccount(account)) {
Toast.makeText(this,R.string.unable_to_update_account,Toast.LENGTH_SHORT).show();
}
}
private void publishOpenPGPPublicKey(Account account) {
if (ManageAccountActivity.this.hasPgp()) {
choosePgpSignId(selectedAccount);
announcePgp(selectedAccount, null, onOpenPGPKeyPublished);
} else {
this.showInstallPgpDialog();
}
@ -349,12 +373,12 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
if (requestCode == REQUEST_CHOOSE_PGP_ID) {
if (data.getExtras().containsKey(OpenPgpApi.EXTRA_SIGN_KEY_ID)) {
selectedAccount.setPgpSignId(data.getExtras().getLong(OpenPgpApi.EXTRA_SIGN_KEY_ID));
announcePgp(selectedAccount, null);
announcePgp(selectedAccount, null, onOpenPGPKeyPublished);
} else {
choosePgpSignId(selectedAccount);
}
} else if (requestCode == REQUEST_ANNOUNCE_PGP) {
announcePgp(selectedAccount, null);
announcePgp(selectedAccount, null, onOpenPGPKeyPublished);
}
this.mPostponedActivityResult = null;
} else {

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