Compare commits
563 commits
feature/fi
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
598e619965 | ||
|
dddff7965f | ||
|
a7fd6b31e2 | ||
|
fdfaae6f59 | ||
|
e2c2ee6507 | ||
|
3a5abac6d9 | ||
|
4f48eb877f | ||
|
be8d156460 | ||
|
626a596b67 | ||
|
c10c335f0d | ||
|
879ffada02 | ||
|
bd139f9b53 | ||
|
e0415d1ac5 | ||
|
ae5947bc7e | ||
|
52329da9e5 | ||
|
68ba8e37bc | ||
|
0071e5bd37 | ||
|
bac1ef1160 | ||
|
af1c1b97b7 | ||
|
089ca1c414 | ||
|
8fcaea2e17 | ||
|
e6ae2427ea | ||
|
d88ea3b078 | ||
|
fa36af1695 | ||
|
e05f1a802b | ||
|
7d03a39302 | ||
|
fa083983a8 | ||
|
bdf31aea33 | ||
|
f7dc10afe6 | ||
|
0b627e818e | ||
|
f114694eb3 | ||
|
8e8ed92308 | ||
|
6850073ab3 | ||
|
e80136862a | ||
|
5d99bff131 | ||
|
6f506d4021 | ||
|
e778db81c7 | ||
|
7208a71045 | ||
|
340510db9c | ||
|
439bd354a1 | ||
|
f816abb047 | ||
|
fb56abb00f | ||
|
ac2f33aa82 | ||
|
3732f8c82b | ||
|
829f4122b3 | ||
|
8b6fbc43bf | ||
|
5ac2ea4ce0 | ||
|
31321c0ef9 | ||
|
6d62eca223 | ||
|
ef57241e15 | ||
|
f083e06215 | ||
|
91123d1229 | ||
|
34ed48900a | ||
|
9c962a4591 | ||
|
6683d4b27c | ||
|
5b305f0548 | ||
|
b9dea9c4f3 | ||
|
03ecff4c18 | ||
|
7507a959b3 | ||
|
9ced052e2b | ||
|
f495a9f762 | ||
|
38fb3e96b0 | ||
|
efafe29c08 | ||
|
ca486709ad | ||
|
8b9749eeec | ||
|
c019e99e10 | ||
|
4dde9eb81f | ||
|
ceae89869c | ||
|
263db81722 | ||
|
db603c330b | ||
|
fa6ead101c | ||
|
a34a28ef1c | ||
|
249c0ac7cd | ||
|
30e4423934 | ||
|
f9b453d879 | ||
|
e431d3d092 | ||
|
49458bb09f | ||
|
a85f0b4d9e | ||
|
493055f2cb | ||
|
7266eba56d | ||
|
86c90ebcf7 | ||
|
12706e565a | ||
|
a6f99f4213 | ||
|
7fa19f4f8c | ||
|
885f22f806 | ||
|
4aebdc5fa0 | ||
|
ebecb6de7a | ||
|
b2cc172423 | ||
|
e64a7c7a45 | ||
|
d729a11d26 | ||
|
01f10a7329 | ||
|
4c6599b395 | ||
|
755625eec5 | ||
|
dfdf8014d8 | ||
|
063ee0bd72 | ||
|
a6a795f605 | ||
|
3931ec3601 | ||
|
7bc7a864ed | ||
|
ca066cef90 | ||
|
9816664d02 | ||
|
e058ee868f | ||
|
260d23bdbc | ||
|
c5460973aa | ||
|
e381d4613e | ||
|
e8e26ab944 | ||
|
83ac39ff73 | ||
|
97cbe0f09d | ||
|
f6a12159ef | ||
|
33d082d446 | ||
|
4ce7c171ec | ||
|
dec454ab82 | ||
|
3ff205a724 | ||
|
9288adbec7 | ||
|
74cdaace14 | ||
|
737ed457a3 | ||
|
30b5f1cc5a | ||
|
defb4c7044 | ||
|
a221959b4a | ||
|
4b34f2351e | ||
|
b8acf9469d | ||
|
6b6483341d | ||
|
55ed4223dd | ||
|
1e86c249f7 | ||
|
efdc848dfb | ||
|
bb472eb1bf | ||
|
b7e2009404 | ||
|
d5bbf8d232 | ||
|
d9cf186293 | ||
|
30b3638941 | ||
|
95d834e8eb | ||
|
4f2d20dbd5 | ||
|
8bcc75c6c8 | ||
|
c5d6c9b026 | ||
|
076e32555d | ||
|
ce38821089 | ||
|
1267228788 | ||
|
0e8e5f8df2 | ||
|
1779e1262c | ||
|
55acdcbbb8 | ||
|
6dff9bc8a9 | ||
|
ad9f8fde90 | ||
|
d941ec1926 | ||
|
43ef8edc7c | ||
|
d189dbf03b | ||
|
649a261706 | ||
|
bb4e06c950 | ||
|
d606577b4d | ||
|
31abe938fd | ||
|
5014248ccb | ||
|
2420f3f2cd | ||
|
c2c8bef66a | ||
|
f3e9134002 | ||
|
435989fc60 | ||
|
15d25f3658 | ||
|
fd2bfdb459 | ||
|
73eaea0e5f | ||
|
d5f1690724 | ||
|
827ae43d8b | ||
|
3f12418b60 | ||
|
dd93aaf507 | ||
|
3abe818259 | ||
|
ed073c9d3b | ||
|
21ddcc478f | ||
|
92b16348f4 | ||
|
e4d3b5ebe8 | ||
|
17f4f4e492 | ||
|
898218e740 | ||
|
71780abb89 | ||
|
993fb18d52 | ||
|
088360b2d6 | ||
|
52e874127b | ||
|
fc777c3a80 | ||
|
e2d89da5d3 | ||
|
caaaa05e81 | ||
|
d03a9819e3 | ||
|
d7a7db1ced | ||
|
9e24597dd2 | ||
|
ea063e8a76 | ||
|
470cc533b2 | ||
|
6e9ea36d92 | ||
|
db952297c5 | ||
|
b10b73caad | ||
|
482cb89267 | ||
|
896d73858d | ||
|
83c1ba7b1c | ||
|
fc0daaf81d | ||
|
afcfd057d8 | ||
|
c3da4be4da | ||
|
d2e1e316fc | ||
|
56a0760fbb | ||
|
ae09585ce2 | ||
|
e96467e5ad | ||
|
2d5a7e013a | ||
|
e125944cbc | ||
|
f0a05e133c | ||
|
ac250ad8c0 | ||
|
a61590ec33 | ||
|
bf420c711a | ||
|
8efe986962 | ||
|
4fac8c47e7 | ||
|
b9d4699d9b | ||
|
ac0c766794 | ||
|
c91f28ea31 | ||
|
8dd82436bb | ||
|
baa5ba17cd | ||
|
3aac32f7aa | ||
|
9a9fc4ed5e | ||
|
2059bdff81 | ||
|
4611713eca | ||
|
22dc081ed1 | ||
|
1e5ebaa19c | ||
|
0da5d84cf4 | ||
|
3741aee815 | ||
|
3c3c645c68 | ||
|
29bc3e9c7b | ||
|
17ab39fe45 | ||
|
64826a6fd9 | ||
|
68d7b433d8 | ||
|
a669e200b5 | ||
|
654b4c12cb | ||
|
880e8b6eb7 | ||
|
9578b015ae | ||
|
40428e101e | ||
|
217e3eee9b | ||
|
857539eac3 | ||
|
94d125945c | ||
|
1373c583e9 | ||
|
ec7cc243ad | ||
|
12c190c85d | ||
|
d01e3bb203 | ||
|
b3ee23d8f0 | ||
|
18be460e10 | ||
|
851f7b073d | ||
|
b0d89ba6ad | ||
|
82e4031e4d | ||
|
2f9d181822 | ||
|
b0d0f904a2 | ||
|
83c976f2d7 | ||
|
6bb9db3686 | ||
|
f5721e6c96 | ||
|
0a2d4589fd | ||
|
efb3183eed | ||
|
cc5e65879d | ||
|
2f27378f16 | ||
|
0b857a81db | ||
|
954cf63162 | ||
|
42c2dd48f0 | ||
|
ac88ed360b | ||
|
b9962f7b50 | ||
|
a917c5403f | ||
|
6ff2ae4e02 | ||
|
67f244555c | ||
|
a6dd8e45fa | ||
|
04595db099 | ||
|
504e4f21a2 | ||
|
99e21438c0 | ||
|
e71bfaa48d | ||
|
6d74698aec | ||
|
f4a207e859 | ||
|
a508f95c3f | ||
|
c172f1ade7 | ||
|
cc78954daa | ||
|
9e752e637f | ||
|
5bf46f8e85 | ||
|
93e97be6db | ||
|
7d8f3720e8 | ||
|
09a7aa6301 | ||
|
e03da284fe | ||
|
bb6c72706d | ||
|
648fe344ae | ||
|
ccdb122afb | ||
|
8cbc7b2a07 | ||
|
dd5f790b21 | ||
|
d80c5fd971 | ||
|
93519d7309 | ||
|
5aa52b0824 | ||
|
8ca04b46e6 | ||
|
a5f52ef599 | ||
|
1657e678ac | ||
|
0f92f4a57c | ||
|
3fde191f63 | ||
|
b4f8f47540 | ||
|
15d27e2cbb | ||
|
80c0e7d575 | ||
|
23cfd788b1 | ||
|
99cfae2e33 | ||
|
07b4ea61b2 | ||
|
5abd54f943 | ||
|
b8602852b7 | ||
|
dc44c346ca | ||
|
65bfbafa17 | ||
|
51f52022cb | ||
|
cffb75e456 | ||
|
cbffac53d5 | ||
|
887b14dae0 | ||
|
3a6c10bbe3 | ||
|
fe080ef77a | ||
|
b9247bc3a7 | ||
|
6c3565392a | ||
|
0c9d000689 | ||
|
20b0c52e35 | ||
|
c1b0151474 | ||
|
6368dedc63 | ||
|
fe6724887d | ||
|
960460e7fe | ||
|
e0e158e5fc | ||
|
31c16b0f8f | ||
|
32b1ab2b3b | ||
|
d6652c571d | ||
|
4f0ac410eb | ||
|
6c445cc68a | ||
|
ad32a6889b | ||
|
5d5b7010b1 | ||
|
6084fd91d4 | ||
|
074e7311bf | ||
|
2cdc394d52 | ||
|
b5c5625e78 | ||
|
d828d5b4a9 | ||
|
e66ffc6280 | ||
|
cd1d3cebee | ||
|
d8d37876b0 | ||
|
8358a40792 | ||
|
7ee4de1a27 | ||
|
a80432f671 | ||
|
348498b875 | ||
|
539302ee36 | ||
|
587a719e85 | ||
|
062371dd4c | ||
|
6690201ede | ||
|
c0c45b4eab | ||
|
5d83478952 | ||
|
7ea00aa98c | ||
|
14081b1f85 | ||
|
d66581e9e6 | ||
|
9213106193 | ||
|
123fd1926f | ||
|
0d649e48bb | ||
|
da925991c1 | ||
|
74251e61d4 | ||
|
7abea021ce | ||
|
5c7d6141e0 | ||
|
5aae0c8047 | ||
|
449f09c975 | ||
|
78c85da03c | ||
|
ba225074d5 | ||
|
60b96381f3 | ||
|
5c26aec10d | ||
|
345e2b7da1 | ||
|
6c083786f1 | ||
|
8319822427 | ||
|
25babd117d | ||
|
88948711ff | ||
|
cccc24c8e2 | ||
|
f5d5be2b3c | ||
|
b73774260a | ||
|
528649cd70 | ||
|
10a29f53a6 | ||
|
533076813b | ||
|
f42f00b40e | ||
|
6a8fb3e8ae | ||
|
904cc18ec2 | ||
|
189ca534ac | ||
|
4d6a78b005 | ||
|
019e673ce7 | ||
|
1b9c48dbad | ||
|
d66b0d010e | ||
|
0a6879eea5 | ||
|
7d7835e035 | ||
|
ad75ab3f68 | ||
|
6a672b4aac | ||
|
78ef54d600 | ||
|
94b1f705c5 | ||
|
d54b870971 | ||
|
d69f35db35 | ||
|
cdcc4979ec | ||
|
87345e410e | ||
|
1c0157d2ab | ||
|
5d67ddc1b1 | ||
|
06949b7649 | ||
|
f81d60f9a8 | ||
|
b1f1c773f8 | ||
|
d69583c3f3 | ||
|
7bb48465e2 | ||
|
6e386a8701 | ||
|
267f920208 | ||
|
1f7259407f | ||
|
ccc1254896 | ||
|
c313ffadec | ||
|
f2ceba6a90 | ||
|
6929196af0 | ||
|
131102939c | ||
|
0389f67482 | ||
|
c85b487fca | ||
|
001da15a33 | ||
|
bbc79c5ee1 | ||
|
e336d96890 | ||
|
824ba44fd4 | ||
|
8e2adfcfd1 | ||
|
aac524da7e | ||
|
1076c25767 | ||
|
ac86587323 | ||
|
e5cb9b1e17 | ||
|
5ac2a42d13 | ||
|
7d8514492a | ||
|
493ac6286f | ||
|
57e0f4a21d | ||
|
004867686b | ||
|
9f45e2509e | ||
|
35df965b58 | ||
|
8d7727fcdc | ||
|
6df9c91b30 | ||
|
5e41a659b7 | ||
|
82639b94b7 | ||
|
7c8b91325e | ||
|
3b6b25720f | ||
|
41e0559533 | ||
|
25eb08ae48 | ||
|
c488121d38 | ||
|
4ca1c1420f | ||
|
cf2451fa8c | ||
|
2c2bb185c3 | ||
|
5a3f721eb4 | ||
|
6a4b714e10 | ||
|
4f4683e052 | ||
|
40446c25d9 | ||
|
d76882631c | ||
|
616eba9d1f | ||
|
60ca56c188 | ||
|
cb0be6f06a | ||
|
ae45a3e55b | ||
|
ee84b68caf | ||
|
89781841ac | ||
|
7375f24041 | ||
|
d852745b2c | ||
|
c5c8ba4f31 | ||
|
f3a821d11e | ||
|
72f2b8650e | ||
|
2565da03bb | ||
|
5aa83a0db4 | ||
|
0fec7bcea2 | ||
|
9ce21a8077 | ||
|
59f392a17a | ||
|
7286bf019e | ||
|
443925d2e2 | ||
|
104c02dc33 | ||
|
80e61023c5 | ||
|
bf260e9341 | ||
|
8b8d17900e | ||
|
9fd788bf7e | ||
|
b0347ba929 | ||
|
5bf81ac737 | ||
|
523f5a14a2 | ||
|
541ac49fed | ||
|
c5c9c92b26 | ||
|
5289ddd6d8 | ||
|
041351b176 | ||
|
82ff5d1238 | ||
|
9f7fcd1a47 | ||
|
c75fbc7e57 | ||
|
346b272bac | ||
|
c0c1af1cca | ||
|
008d289514 | ||
|
b26b5d01fe | ||
|
e0989e7a82 | ||
|
639f3c485b | ||
|
1c9b10c79d | ||
|
f976ebae8a | ||
|
28a2686620 | ||
|
701b8617de | ||
|
517eff346f | ||
|
53e19a2e9c | ||
|
03530f667d | ||
|
2a55037993 | ||
|
f65fa8fb0b | ||
|
62209dc7ac | ||
|
eb34cdc3e3 | ||
|
1b45c33393 | ||
|
b37741a0ae | ||
|
092beba1ba | ||
|
88574e39d2 | ||
|
8857f9341c | ||
|
85b8ec7702 | ||
|
0abffde5ef | ||
|
f7da662e6a | ||
|
bc272651e2 | ||
|
911b537f34 | ||
|
41f36bd816 | ||
|
3334ab7d0a | ||
|
9da5429893 | ||
|
c9a81b53b3 | ||
|
ea4b999d8a | ||
|
1584df8ac7 | ||
|
8427f377ea | ||
|
5606b32db7 | ||
|
7aaca90e41 | ||
|
0559e801fa | ||
|
fce79f08c1 | ||
|
41aecb003d | ||
|
e93057cfce | ||
|
46232857fc | ||
|
1b58f476c9 | ||
|
079777d9dd | ||
|
e07828271d | ||
|
361eec24a9 | ||
|
1322031176 | ||
|
75548eafa4 | ||
|
c7629a6093 | ||
|
f8a94ec52e | ||
|
b33eaff4f4 | ||
|
a2ff79a4c3 | ||
|
8e854ba29d | ||
|
21ecf54e37 | ||
|
be82ac5633 | ||
|
4aa24e84c4 | ||
|
9cd6fa5097 | ||
|
f36ff9640d | ||
|
71e4bcf9cb | ||
|
edcfdb974d | ||
|
ae2a4ed495 | ||
|
7da9909586 | ||
|
e612506be3 | ||
|
506b092821 | ||
|
987744bb25 | ||
|
cce36fcbb7 | ||
|
24ca2a88fb | ||
|
a1ade22308 | ||
|
4323b7de5a | ||
|
240addee71 | ||
|
ca21a38cb6 | ||
|
4142360189 | ||
|
016124b548 | ||
|
6c3b5defa3 | ||
|
7610493b1e | ||
|
611f28fbd0 | ||
|
b519d6370b | ||
|
6d83b098e6 | ||
|
a65e9edef1 | ||
|
75923a1835 | ||
|
9acde9e105 | ||
|
bc7c378080 | ||
|
4210498d3c | ||
|
58b464cf26 | ||
|
8b63dbb26d | ||
|
94143682ff | ||
|
47c29e5842 | ||
|
f7c293387b | ||
|
dd627f76bf | ||
|
037cd4e95b | ||
|
fa07274d2d | ||
|
6e93f698a1 | ||
|
1f77d6b8cd | ||
|
f42d144a66 | ||
|
d52f09b6f4 | ||
|
c88523b1b8 | ||
|
00334edc6f | ||
|
7d55a62328 | ||
|
e8a4eaf8cd | ||
|
f2c4eebaf8 | ||
|
6086d9c45f | ||
|
686c4da2b0 | ||
|
037932dc02 | ||
|
66a57e0129 | ||
|
226d45a136 |
|
@ -9,7 +9,7 @@ jobs:
|
|||
key: gradle-{{ checksum "build.gradle" }}-{{ checksum ".circleci/config.yml" }}
|
||||
- run: export GRADLE_USER_HOME=$PWD/.gradle
|
||||
- run: echo y | sdkmanager "platforms;android-$(sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' build.gradle)" > /dev/null
|
||||
- run: ./gradlew lintStandardDebug
|
||||
- run: ./gradlew lintGitDebug
|
||||
- save_cache:
|
||||
paths:
|
||||
- .gradle/caches
|
||||
|
@ -28,13 +28,13 @@ jobs:
|
|||
- run: export GRADLE_USER_HOME=$PWD/.gradle
|
||||
- run: echo y | sdkmanager "platforms;android-$(sed -n 's,.*compileSdkVersion\s*\([0-9][0-9]*\).*,\1,p' build.gradle)" > /dev/null
|
||||
# build
|
||||
- run: ./gradlew assembleStandard
|
||||
- run: ./gradlew assembleGit
|
||||
- save_cache:
|
||||
paths:
|
||||
- ~/.android
|
||||
key: android
|
||||
- store_artifacts:
|
||||
path: build/outputs/apk/standard
|
||||
path: build/outputs/apk/git
|
||||
|
||||
publish:
|
||||
docker:
|
||||
|
@ -52,7 +52,7 @@ jobs:
|
|||
- run: sed -i "0,/versionName/s/^\(\s*versionName\).*/\1 \"$(printf '%s-%05d' $(git describe --tag --abbrev=0) $(git rev-list --first-parent --count HEAD))\"/" build.gradle
|
||||
- run: cat -n build.gradle
|
||||
# build
|
||||
- run: ./gradlew assembleStandard
|
||||
- run: ./gradlew assembleGit
|
||||
# publish on nightly fdroid repo
|
||||
- run: fdroid nightly
|
||||
- save_cache:
|
||||
|
|
71
CHANGELOG.md
|
@ -1,5 +1,72 @@
|
|||
### Changelog
|
||||
|
||||
#### Version 2.3.7
|
||||
* bug fixes
|
||||
|
||||
#### Version 2.3.6
|
||||
* bug fixes
|
||||
|
||||
#### Version 2.3.5
|
||||
* start removing OTR
|
||||
* rework conference and contact details (big avatar is available via long click) (PAM)
|
||||
* resume download of OMEMO encrypted files
|
||||
* channels now use '#' as symbol in avatar
|
||||
* support for ?register and ?register;preauth XMPP uri parameters
|
||||
* update connection settings
|
||||
* use ExoPlayer for video playback (PAM)
|
||||
* show artist - title for audio files (PAM)
|
||||
* show PDF previews (PAM)
|
||||
* minor UI improvements (PAM)
|
||||
* use 12 byte IV for OMEMO
|
||||
* a lot of bug fixes
|
||||
|
||||
#### Version 2.3.4
|
||||
* fixes for Jingle IBB file transfer
|
||||
* fixes for repeated corrections filling up the database
|
||||
* switched to Last Message Correction v1.1
|
||||
* increase mam messages catchup (PAM)
|
||||
* bug fixes
|
||||
|
||||
#### Version 2.3.3
|
||||
* fix missing send button
|
||||
|
||||
#### Version 2.3.2
|
||||
* fix shareWithActivity
|
||||
|
||||
#### Version 2.3.1
|
||||
* bug fixes
|
||||
|
||||
#### Version 2.3.0
|
||||
* show name in quotes (PAM)
|
||||
* introduce theme based on systems theme (PAM)
|
||||
* increase default video quality (720p instead of 360p) (PAM)
|
||||
* replace YouTube links with Invidious links (PAM)
|
||||
* rework profile view (PAM)
|
||||
* introduce app intro and some help screens (PAM)
|
||||
* fixed minor security issues
|
||||
* share XMPP uri from channel search by long pressing a result
|
||||
* fixed OMEMO self healing (after backup restore) on servers w/o MAM
|
||||
* introduce expert setting to perform channel discovery on local server instead of [search.jabber.network](https://search.jabber.network)
|
||||
* introduce new orange theme color (PAM)
|
||||
* bug fixes
|
||||
|
||||
#### Version 2.2.9
|
||||
* bug fixes
|
||||
|
||||
#### Version 2.2.8
|
||||
* stability improvements for group chats and channels
|
||||
* allow backups to be restored from anywhere
|
||||
* make short vibrate in open chat configurable and respect silent mode (PAM)
|
||||
* fixes for Jingle file transfer
|
||||
* fixed some rare crashes
|
||||
* when sharing a message from and to messenger insert it as quote
|
||||
* find orphaned files and show them in the chat again instead of showing them deleted (PAM)
|
||||
* introduce file uploads/downloads with queue (PAM)
|
||||
* fixed connection issues over Tor
|
||||
* P2P file transfer (Jingle) now offers direct candidates
|
||||
* support XEP-0396: Jingle Encrypted Transports - OMEMO
|
||||
* bug fixes
|
||||
|
||||
#### Version 2.2.7
|
||||
* fixing crashes
|
||||
|
||||
|
@ -8,7 +75,7 @@
|
|||
* fix broken updater
|
||||
|
||||
#### Version 2.2.5
|
||||
* make backup compatible to Conversations (only works for Android >= 8)
|
||||
* make backup compatible to Conversations (only works for Android >= 8) (PAM)
|
||||
* bug fix
|
||||
|
||||
#### Version 2.2.4
|
||||
|
@ -18,7 +85,7 @@
|
|||
* set own OMEMO devices to inactive after not seeing them for 60 days. (was 7 days)
|
||||
* bug fixes for peer to peer file transfer (Jingle)
|
||||
* fixed server info for unlimited/unknown max file size
|
||||
* make backup compatible to Conversations
|
||||
* make backup compatible to Conversations (PAM)
|
||||
* performance improvements
|
||||
* bug fixes
|
||||
|
||||
|
|
140
README-en.md
Normal file
|
@ -0,0 +1,140 @@
|
|||
![logo](/art/icon.png)
|
||||
# Pix-Art Messenger [![CircleCI](https://circleci.com/gh/kriztan/Pix-Art-Messenger/tree/master.svg?style=shield)](https://circleci.com/gh/kriztan/Pix-Art-Messenger/tree/master)
|
||||
|
||||
🇩🇪 <a href='README.md' style="font-size:150%">Deutsche Version der Readme hier verfügbar.</a>
|
||||
|
||||
Pix-Art Messenger is a fork of [Conversations](https://github.com/siacs/Conversations).
|
||||
The changes aim to improve usability and ease transition from pre-installed and other widespread messengers. Here are some screenshots:
|
||||
|
||||
<img src="metadata/en-US/phoneScreenshots/00.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/01.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/02.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/03.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/04.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/05.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/06.png" width="200" />
|
||||
|
||||
(Images used were taken from http://freestockgallery.de)
|
||||
|
||||
## Download
|
||||
Pix-Art is available for install in the F-Droid and Google-Play stores.
|
||||
Alternatively release and beta-release APKs are available via github: [Releases](https://github.com/kriztan/Pix-Art-Messenger/releases/latest)
|
||||
|
||||
<a href='https://play.google.com/store/apps/details?id=de.pixart.messenger'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height="100"/></a> <a href="https://f-droid.org/app/de.pixart.messenger"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a>
|
||||
|
||||
#### Pix-Art-Messenger nightly and beta
|
||||
|
||||
##### F-Droid
|
||||
Scan the QR-Code below and add it to your F-Droid repositories.
|
||||
|
||||
<img src="https://raw.githubusercontent.com/kriztan/Pix-Art-Messenger-nightly/master/icon.png" width="220">
|
||||
|
||||
#### Google Play
|
||||
Google-Users can join the Google-Play beta program:
|
||||
[Pix-Art Messenger beta](https://play.google.com/apps/testing/de.pixart.messenger)
|
||||
|
||||
|
||||
## Social Media
|
||||
<a rel="me" href="https://social.tchncs.de/@pixart">Pix-Art Messenger on Mastodon (German)</a>
|
||||
|
||||
There are also English and German speaking XMPP-MUCs focusing on support and development of the Pix-Art Messenger.
|
||||
|
||||
If you are interested in the development of the messenger, here is a MUC for you (English and German speaking):
|
||||
|
||||
Development-Chat: [development@room.pix-art.de](https://jabber.pix-art.de/j/development@room.pix-art.de?join)
|
||||
[![Users in muc](https://inverse.chat/badge.svg?room=development@room.pix-art.de)](https://jabber.pix-art.de/j/development@room.pix-art.de?join)
|
||||
|
||||
|
||||
There also is an Support-MUC where you can ask questions and get help with issues you may encounter, see further below for details.
|
||||
|
||||
|
||||
## How can I support translations ?
|
||||
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/pix-art-messenger/localized.svg)](https://crowdin.com/project/pix-art-messenger)
|
||||
|
||||
Translation of in-app text is organised via [crowdin.com](https://crowdin.com/project/pix-art-messenger). You can add new languages as locales and add and edit translations already existing.
|
||||
|
||||
[Here is the project page on crowdin.com](https://crowdin.com/project/pix-art-messenger/invite?d=75l6j4k6k6k523f4j4m4e473u663d3m4p4t4q4)
|
||||
|
||||
## Help! I've encountered issues!
|
||||
The easiest way to get some help is to join our support-MUC (both English and German).
|
||||
|
||||
Support-Chat invite link: [support@room.pix-art.de](https://jabber.pix-art.de/j/support@room.pix-art.de?join)
|
||||
[![Users in muc](https://inverse.chat/badge.svg?room=support@room.pix-art.de)](https://jabber.pix-art.de/j/support@room.pix-art.de?join)
|
||||
|
||||
Or scan this QR-Code:
|
||||
|
||||
<img src="art/qr-code_suport_small.png" width="220">
|
||||
|
||||
If we can't fix your problem there, you can open an issue here on github, detailing your problem, how to reproduce it and provide logs. See instructions below on how to create log files.
|
||||
|
||||
|
||||
|
||||
### How to create debug logs? (adb)
|
||||
|
||||
#### GNU/Linux, OSX and other Unix-like systems:
|
||||
|
||||
1. First install The **A**ndroid **D**ebugging **B**ridge, if not already present.
|
||||
###### Ubuntu / Linux Mint
|
||||
```
|
||||
sudo apt-get update
|
||||
sudo apt-get install android-tools-adb
|
||||
```
|
||||
###### openSUSE 42.2 and 42.3
|
||||
```
|
||||
sudo zypper ref
|
||||
sudo zypper install android-tools
|
||||
```
|
||||
###### openSUSE Tumbleweed
|
||||
here you need to add the following repo (e.g. via Yast):
|
||||
http://download.opensuse.org/repositories/hardware/openSUSE_Tumbleweed/
|
||||
|
||||
alternatively you have the option to use the `1 Click installer`
|
||||
https://software.opensuse.org/package/android-tools
|
||||
###### other systems
|
||||
install adb using a method appropriate for your system
|
||||
|
||||
2. Now open a terminal in a directory of you're choice, or navigate to the directory using `cd`.
|
||||
|
||||
3. Follow steps [6] to [10] of the Windows instructions.
|
||||
|
||||
4. Start outputting your log to a file on your computer. We will be using `logcat.txt`. Enter:
|
||||
```
|
||||
$ adb -d logcat -v time | grep -i Pix-Art > logcat.txt
|
||||
```
|
||||
|
||||
5. Follow the remaining steps [12] and [13] of the Windows instructions.
|
||||
|
||||
|
||||
#### Windows:
|
||||
|
||||
1. Download Google's SDK-platform tools for your operating system:
|
||||
|
||||
https://developer.android.com/studio/releases/platform-tools.html
|
||||
2. In case they were not included: You also need the ADB_drivers for your version of Microsoft Windows:
|
||||
|
||||
https://developer.android.com/studio/run/win-usb.html
|
||||
3. Extract the zip-archive (e.g. to `C:\ADB\`)
|
||||
4. Open the command line (CMD) using the start menu: Start > Execute: cmd
|
||||
5. Navigate to the directory you extracted the zip to as following. We will be using `C:\ADB\`
|
||||
```
|
||||
c:
|
||||
cd ADB
|
||||
```
|
||||
6. On your smartphone open the settings and search for the item `Developer Options`. If this option is not already present on your phone you will need to unlock it beforehand. To do this navigate to `Settings > About phone`, there locate `Build number` (or similar) and tap it 7-times in succession. You should now see a notification, that you are now a developer. Congrats, `Developer Options` are now available in your settings menu.
|
||||
7. Inside `Developer Options` search activate the setting `USB-Debugging` (sometimes just called `Android Debugging`).
|
||||
8. Connect your phone to your computer via USB cable. The necessary drivers should now be downloaded and installed if not already present. On Windows all necessary drivers should be downloaded automatically if you followed step [2] beforehand. On most GNU/Linux systems no additional action is required.
|
||||
9. If everything worked out, you can now return to the command line and test if your device is being recognised. Enter `adb devices -l`; you should see output similar to:
|
||||
```
|
||||
> adb devices -l
|
||||
List of devices attached
|
||||
* daemon not running. starting it now on port 5037 *
|
||||
* daemon started successfully *
|
||||
042111560169500303f4 unauthorized
|
||||
```
|
||||
10. If your devices is labelled as `unautorized`, you must first accept a prompt on your phone asking if debugging over USB should be allowed. When rerunning `adb devices` you should now see:
|
||||
```
|
||||
> adb devices
|
||||
List of devices attached
|
||||
042111560169500303f4 device
|
||||
```
|
||||
11. Start outputting your log to a file on your computer. We will be using `logcat.txt` in `C:\ADB\`. Just enter the following (without `> ` into the command line):
|
||||
```
|
||||
> adb -d logcat -v time | FINDSTR Pix-Art > logcat.txt
|
||||
```
|
||||
12. Now reproduce the issue encountered.
|
||||
|
||||
13. Stop logging. Now take a close look at your log file and remove any personal and private information you may find before sending it together with a detailed description of your issue, instructions on how to reproduce to me. You can use GitHub's issue tracker: [Issues](https://github.com/kriztan/Pix-Art-Messenger/issues)
|
|
@ -1,6 +1,8 @@
|
|||
![logo](/art/icon.png)
|
||||
# Pix-Art Messenger [![CircleCI](https://circleci.com/gh/kriztan/Pix-Art-Messenger/tree/master.svg?style=shield)](https://circleci.com/gh/kriztan/Pix-Art-Messenger/tree/master)
|
||||
|
||||
🇬🇧🇺🇸… <a href='README-en.md' style="font-size:150%">English Readme version available here</a>
|
||||
|
||||
Pix-Art Messenger ist eine Kopie der offiziellen Android-App [Conversations](https://github.com/siacs/Conversations) mit einigen Änderungen, insbesondere zur Verbesserung der Benutzerfreundlichkeit, um den Umstieg von oftmals vorinstallierten Messengern zu erleichtern. Die folgenden Bilder geben erste Eindrücke der App:
|
||||
|
||||
<img src="metadata/en-US/phoneScreenshots/00.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/01.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/02.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/03.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/04.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/05.png" width="200" /> <img src="metadata/en-US/phoneScreenshots/06.png" width="200" />
|
||||
|
@ -9,7 +11,7 @@ Pix-Art Messenger ist eine Kopie der offiziellen Android-App [Conversations](htt
|
|||
|
||||
Download ist hier möglich:
|
||||
|
||||
<a href='https://play.google.com/store/apps/details?id=de.pixart.messenger'><img alt='Get it on Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png' height="100"/></a> <a href="https://f-droid.org/app/de.pixart.messenger"><img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="100"></a>
|
||||
<a href='https://play.google.com/store/apps/details?id=de.pixart.messenger'><img alt='Jeztz bei Google Play' src='https://play.google.com/intl/en_us/badges/images/generic/de_badge_web_generic.png' height="100"/></a> <a href="https://f-droid.org/app/de.pixart.messenger"><img src="https://f-droid.org/badge/get-it-on-de.png" alt="Jetzt bei F-Droid" height="100"></a>
|
||||
|
||||
Alternativ kannst du den Messenger auch direkt hier von GitHub unter [Releases](https://github.com/kriztan/Pix-Art-Messenger/releases/latest) herunterladen.
|
||||
|
||||
|
|
BIN
art/Feature_OMEMO.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
art/schulchat/logo.png
Normal file
After Width: | Height: | Size: 12 KiB |
93
build.gradle
|
@ -6,7 +6,7 @@ buildscript {
|
|||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.4.1'
|
||||
classpath 'com.android.tools.build:gradle:3.6.1'
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,59 +33,63 @@ repositories {
|
|||
}
|
||||
|
||||
configurations {
|
||||
standardPushImplementation
|
||||
playstoreImplementation
|
||||
gitImplementation
|
||||
compile.exclude group: 'org.jetbrains' , module:'annotations'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':libs:android-transcoder')
|
||||
standardPushImplementation ('com.google.firebase:firebase-messaging:17.3.4') {
|
||||
playstoreImplementation('com.google.firebase:firebase-messaging:20.1.3') {
|
||||
exclude group: 'com.google.firebase', module: 'firebase-core'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-analytics'
|
||||
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
|
||||
}
|
||||
playstoreImplementation 'com.android.installreferrer:installreferrer:1.1.2'
|
||||
implementation 'org.sufficientlysecure:openpgp-api:10.0'
|
||||
implementation('com.theartofdev.edmodo:android-image-cropper:2.7.+') {
|
||||
implementation('com.theartofdev.edmodo:android-image-cropper:2.8.0') {
|
||||
exclude group: 'com.android.support', module: 'appcompat-v7'
|
||||
exclude group: 'com.android.support', module: 'exifinterface'
|
||||
}
|
||||
implementation 'org.bouncycastle:bcmail-jdk15on:1.58'
|
||||
implementation 'org.bouncycastle:bcmail-jdk15on:1.64'
|
||||
implementation 'org.jitsi:org.otr4j:0.22'
|
||||
implementation 'org.gnu.inet:libidn:1.15'
|
||||
implementation 'com.google.zxing:core:3.3.3'
|
||||
implementation 'com.google.zxing:core:3.3.3' // > 3.3.x not working below SDK 24
|
||||
implementation 'de.measite.minidns:minidns-hla:0.2.4'
|
||||
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
|
||||
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
|
||||
implementation 'com.makeramen:roundedimageview:2.3.0'
|
||||
implementation 'jetty:javax.servlet:5.1.12'
|
||||
implementation 'com.google.code.gson:gson:2.8.2'
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
implementation 'com.android.support:support-v13:28.0.0'
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
implementation 'com.android.support:exifinterface:28.0.0'
|
||||
implementation 'com.android.support:support-v4:28.0.0'
|
||||
implementation 'com.android.support:support-emoji:28.0.0'
|
||||
implementation 'com.android.support:support-emoji-bundled:28.0.0'
|
||||
implementation 'com.android.support:support-emoji-appcompat:28.0.0'
|
||||
implementation 'com.android.support:exifinterface:28.0.0'
|
||||
implementation 'com.android.support:design:28.0.0'
|
||||
implementation 'com.android.support:cardview-v7:28.0.0'
|
||||
implementation 'com.google.code.gson:gson:2.8.6'
|
||||
implementation 'androidx.multidex:multidex:2.0.1'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'androidx.emoji:emoji:1.0.0'
|
||||
gitImplementation 'androidx.emoji:emoji-appcompat:1.0.0'
|
||||
gitImplementation 'androidx.emoji:emoji-bundled:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.0.0' // higher versions cause strange fab design
|
||||
implementation 'androidx.cardview:cardview:1.0.0' // for compatibility
|
||||
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
|
||||
implementation project(':libs:fullscreenvideoview')
|
||||
implementation 'pub.devrel:easypermissions:2.0.0'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-core:2.11.3'
|
||||
implementation 'com.google.android.exoplayer:exoplayer-ui:2.11.3'
|
||||
implementation 'pub.devrel:easypermissions:3.0.0' // version >= 3.0.0 needs android X libraries
|
||||
implementation 'com.wefika:flowlayout:0.4.1'
|
||||
implementation 'com.googlecode.ez-vcard:ez-vcard:0.10.5'
|
||||
implementation project(':libs:xmpp-addr')
|
||||
implementation 'org.hsluv:hsluv:0.2'
|
||||
implementation 'org.conscrypt:conscrypt-android:1.4.2'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.2.1'
|
||||
implementation 'pl.droidsonroids.gif:android-gif-drawable:1.2.15' // 1.2.15 is last working version for minSDK 16
|
||||
implementation 'me.drakeet.support:toastcompat:1.1.0'
|
||||
implementation 'org.osmdroid:osmdroid-android:6.0.3'
|
||||
implementation 'com.leinardi.android:speed-dial:2.0.1'
|
||||
implementation 'org.osmdroid:osmdroid-android:6.1.5'
|
||||
implementation 'com.leinardi.android:speed-dial:3.1.1' // version >= 3.0.0 needs android X libraries
|
||||
implementation 'com.squareup.picasso:picasso:2.71828'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.2' // versions > 3.12.x don't support API level < 21 anymore
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.5.0'
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
|
||||
implementation 'com.google.guava:guava:27.1-android'
|
||||
implementation 'com.squareup.okhttp3:okhttp:3.12.10' // versions > 3.12.x don't support API level < 21 anymore
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.6.4' //retrofit needs to stick with 2.6.x (https://github.com/square/retrofit/blob/master/CHANGELOG.md)
|
||||
implementation 'com.squareup.retrofit2:converter-gson:2.6.4'
|
||||
implementation 'com.google.guava:guava:28.2-android'
|
||||
implementation 'com.github.AppIntro:AppIntro:5.1.0'
|
||||
}
|
||||
|
||||
ext {
|
||||
|
@ -95,32 +99,48 @@ ext {
|
|||
|
||||
android {
|
||||
|
||||
compileSdkVersion 28
|
||||
compileSdkVersion 29
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
targetSdkVersion 29
|
||||
|
||||
versionCode 266
|
||||
versionName "2.2.7"
|
||||
versionCode 299
|
||||
versionName "2.3.7"
|
||||
versionNameSuffix "beta_(2020-04-01)" // "-beta_(XXXX-XX-XX)"
|
||||
//resConfigs "en"
|
||||
|
||||
archivesBaseName += "-$versionName"
|
||||
//archivesBaseName += "-$versionNameSuffix" // activate for beta versions
|
||||
applicationId "de.pixart.messenger"
|
||||
multiDexEnabled true
|
||||
|
||||
buildConfigField("String", "LOGTAG", '"Pix-Art_Messenger"')
|
||||
buildConfigField("String", "DOMAIN_LOCK", 'null')
|
||||
buildConfigField("String", "MAGIC_CREATE_DOMAIN", '"blabber.im"')
|
||||
buildConfigField("boolean", "SHOW_INTRO", 'true')
|
||||
buildConfigField("String", "UPDATE_URL", '"https://xmpp.pix-art.de/Pix-Art_Messenger/update/"')
|
||||
resValue "string", "applicationId", applicationId
|
||||
resValue "string", "app_name", "Pix-Art Messenger"
|
||||
dimension "distribution"
|
||||
}
|
||||
|
||||
dataBinding {
|
||||
enabled true
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
//X86
|
||||
exclude "lib/x86/**"
|
||||
//X86_64
|
||||
exclude "lib/x86_64/**"
|
||||
//armeabi
|
||||
exclude "lib/armeabi/**"
|
||||
}
|
||||
|
||||
dexOptions {
|
||||
// Skip pre-dexing when running on Travis CI or when disabled via -Dpre-dex=false.
|
||||
preDexLibraries = preDexEnabled && !travisBuild
|
||||
javaMaxHeapSize "8g"
|
||||
javaMaxHeapSize "4g"
|
||||
jumboMode true
|
||||
}
|
||||
|
||||
|
@ -132,10 +152,13 @@ android {
|
|||
flavorDimensions("distribution")
|
||||
|
||||
productFlavors {
|
||||
standardPush {
|
||||
playstore {
|
||||
dimension "distribution"
|
||||
versionNameSuffix "-playstore"
|
||||
}
|
||||
git {
|
||||
dimension "distribution"
|
||||
}
|
||||
standard
|
||||
}
|
||||
if (project.hasProperty('mStoreFile') &&
|
||||
project.hasProperty('mStorePassword') &&
|
||||
|
|
18
docs/encryption.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
Dear users,
|
||||
|
||||
Pix-Art Messenger will remove the implementation of OTR encryption by 30th June of 2020.
|
||||
|
||||
Unfortunately, it does not make sense to continue support on a rather out-dated technology, even as we see some users keep using it. For the moment, OTR (as well as OpenPGP) can be activated via the expert settings for advanced users. Pix-Art Messenger always tries to be usable for even [non-technical users](https://github.com/kriztan/Pix-Art-Messenger/issues/227), OTR however is not counted as appropriate for this.
|
||||
|
||||
Please consider to use OMEMO in the future, many other clients has implemented this, have a look at [omemo.top](https://omemo.top/). However, if you really need to continue using it, please refer to e.g. [Miranda](https://www.miranda-ng.org/de/), [Pidgin plugin](https://github.com/gkdr/lurch/), [Profanity](https://profanity-im.github.io/) or [Coy.im](https://coy.im/). You are also able to fork Pix-Art Messenger and continue the implementation on your own.
|
||||
|
||||
Please consider the differences between encryption protocols and also advantages of OMEMO:
|
||||
|
||||
<img src="https://github.com/kriztan/Pix-Art-Messenger/blob/master/art/Feature_OMEMO.png" width="400">
|
||||
Source: [https://conversations.im/omemo](https://conversations.im/omemo)
|
||||
|
||||
Some limitations of OTR: [wikipedia.org/wiki/Off-the-Record](https://en.wikipedia.org/wiki/Off-the-Record_Messaging#Limitations)
|
||||
|
||||
Please inform your contacts who may also use it.
|
||||
|
||||
Mastodon: https://social.tchncs.de/@pixart
|
|
@ -1 +1,6 @@
|
|||
android.enableJetifier=true
|
||||
android.useAndroidX=true
|
||||
org.gradle.jvmargs=-Xmx2048M
|
||||
org.gradle.parallel=true
|
||||
android.enableR8=true
|
||||
android.enableR8.fullMode=false
|
||||
|
|
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -1,6 +1,6 @@
|
|||
#Fri Apr 26 22:25:54 CEST 2019
|
||||
#Fri Sep 06 22:42:08 CEST 2019
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:support-v13:28.0.0'
|
||||
implementation 'androidx.legacy:legacy-support-v13:1.0.0'
|
||||
}
|
||||
|
||||
android {
|
||||
|
|
|
@ -19,11 +19,9 @@ import android.media.MediaExtractor;
|
|||
import android.media.MediaFormat;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.media.MediaMuxer;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import net.ypresto.androidtranscoder.format.MediaFormatStrategy;
|
||||
import net.ypresto.androidtranscoder.utils.ISO6709LocationParser;
|
||||
import net.ypresto.androidtranscoder.utils.MediaExtractorUtils;
|
||||
|
||||
import java.io.FileDescriptor;
|
||||
|
@ -139,18 +137,6 @@ public class MediaTranscoderEngine {
|
|||
// skip
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
String locationString = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_LOCATION);
|
||||
if (locationString != null) {
|
||||
float[] location = new ISO6709LocationParser().parse(locationString);
|
||||
if (location != null) {
|
||||
mMuxer.setLocation(location[0], location[1]);
|
||||
} else {
|
||||
Log.d(TAG, "Failed to parse the location metadata: " + locationString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
mDurationUs = Long.parseLong(mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)) * 1000;
|
||||
} catch (NumberFormatException e) {
|
||||
|
|
|
@ -18,9 +18,10 @@ package net.ypresto.androidtranscoder.format;
|
|||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaFormat;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
class AndroidStandardFormatStrategy implements MediaFormatStrategy {
|
||||
public static final int AUDIO_BITRATE_AS_IS = -1;
|
||||
public static final int AUDIO_CHANNELS_AS_IS = -1;
|
||||
|
|
1
libs/fullscreenvideoview/.gitignore
vendored
|
@ -1 +0,0 @@
|
|||
/build
|
|
@ -1,27 +0,0 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
compileSdkVersion 28
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
targetSdkVersion 28
|
||||
versionCode 11
|
||||
versionName "1.1.3"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
//apply from: '../maven_push.gradle'
|
|
@ -1,3 +0,0 @@
|
|||
POM_NAME=FullscreenVideoView Library
|
||||
POM_ARTIFACT_ID=fullscreenvideoview
|
||||
POM_PACKAGING=aar
|
17
libs/fullscreenvideoview/proguard-rules.pro
vendored
|
@ -1,17 +0,0 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# By default, the flags in this file are appended to flags specified
|
||||
# in /Applications/ADT/sdk/tools/proguard/proguard-android.txt
|
||||
# You can edit the include path and order by changing the proguardFiles
|
||||
# directive in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Add any project specific keep options here:
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
|
@ -1,4 +0,0 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.rtoshiro.view.video">
|
||||
|
||||
</manifest>
|
|
@ -1,336 +0,0 @@
|
|||
/**
|
||||
* Copyright (C) 2016 Toshiro Sugii
|
||||
* <p/>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p/>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p/>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.github.rtoshiro.view.video;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaPlayer;
|
||||
import android.os.Handler;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class FullscreenVideoLayout extends FullscreenVideoView implements View.OnClickListener, SeekBar.OnSeekBarChangeListener, MediaPlayer.OnPreparedListener {
|
||||
|
||||
/**
|
||||
* Log cat TAG name
|
||||
*/
|
||||
private final static String TAG = "FullscreenVideoLayout";
|
||||
|
||||
/**
|
||||
* RelativeLayout that contains all control related views
|
||||
*/
|
||||
public View videoControlsView;
|
||||
|
||||
/**
|
||||
* SeekBar reference (from videoControlsView)
|
||||
*/
|
||||
protected SeekBar seekBar;
|
||||
|
||||
/**
|
||||
* Reference to ImageButton play
|
||||
*/
|
||||
protected ImageButton imgplay;
|
||||
|
||||
/**
|
||||
* Reference to ImageButton fullscreen
|
||||
*/
|
||||
protected ImageButton imgfullscreen;
|
||||
|
||||
/**
|
||||
* Reference to TextView for elapsed time and total time
|
||||
*/
|
||||
protected TextView textTotal, textElapsed;
|
||||
|
||||
/**
|
||||
* Handler and Runnable to keep tracking on elapsed time
|
||||
*/
|
||||
protected static final Handler TIME_THREAD = new Handler();
|
||||
protected Runnable updateTimeRunnable = new Runnable() {
|
||||
public void run() {
|
||||
updateCounter();
|
||||
|
||||
TIME_THREAD.postDelayed(this, 200);
|
||||
}
|
||||
};
|
||||
|
||||
public FullscreenVideoLayout(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public FullscreenVideoLayout(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public FullscreenVideoLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void release() {
|
||||
super.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initObjects() {
|
||||
super.initObjects();
|
||||
|
||||
if (this.videoControlsView == null) {
|
||||
LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
this.videoControlsView = inflater.inflate(R.layout.view_videocontrols, this, false);
|
||||
}
|
||||
|
||||
if (videoControlsView != null) {
|
||||
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT, RelativeLayout.LayoutParams.WRAP_CONTENT);
|
||||
params.addRule(ALIGN_PARENT_BOTTOM);
|
||||
addView(videoControlsView, params);
|
||||
|
||||
this.seekBar = this.videoControlsView.findViewById(R.id.vcv_seekbar);
|
||||
this.imgfullscreen = this.videoControlsView.findViewById(R.id.vcv_img_fullscreen);
|
||||
this.imgplay = this.videoControlsView.findViewById(R.id.vcv_img_play);
|
||||
this.textTotal = this.videoControlsView.findViewById(R.id.vcv_txt_total);
|
||||
this.textElapsed = this.videoControlsView.findViewById(R.id.vcv_txt_elapsed);
|
||||
}
|
||||
|
||||
if (this.imgplay != null)
|
||||
this.imgplay.setOnClickListener(this);
|
||||
if (this.imgfullscreen != null)
|
||||
this.imgfullscreen.setOnClickListener(this);
|
||||
if (this.seekBar != null)
|
||||
this.seekBar.setOnSeekBarChangeListener(this);
|
||||
|
||||
// Start controls invisible. Make it visible when it is prepared
|
||||
if (this.videoControlsView != null)
|
||||
this.videoControlsView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void releaseObjects() {
|
||||
super.releaseObjects();
|
||||
|
||||
if (this.videoControlsView != null)
|
||||
removeView(this.videoControlsView);
|
||||
}
|
||||
|
||||
protected void startCounter() {
|
||||
Log.d(TAG, "startCounter");
|
||||
|
||||
TIME_THREAD.postDelayed(updateTimeRunnable, 200);
|
||||
}
|
||||
|
||||
protected void stopCounter() {
|
||||
Log.d(TAG, "stopCounter");
|
||||
|
||||
TIME_THREAD.removeCallbacks(updateTimeRunnable);
|
||||
}
|
||||
|
||||
protected void updateCounter() {
|
||||
if (this.textElapsed == null)
|
||||
return;
|
||||
|
||||
int elapsed = getCurrentPosition();
|
||||
// getCurrentPosition is a little bit buggy :(
|
||||
if (elapsed > 0 && elapsed < getDuration()) {
|
||||
seekBar.setProgress(elapsed);
|
||||
|
||||
elapsed = Math.round(elapsed / 1000.f);
|
||||
long s = elapsed % 60;
|
||||
long m = (elapsed / 60) % 60;
|
||||
long h = (elapsed / (60 * 60)) % 24;
|
||||
|
||||
if (h > 0)
|
||||
textElapsed.setText(String.format(Locale.US, "%d:%02d:%02d", h, m, s));
|
||||
else
|
||||
textElapsed.setText(String.format(Locale.US, "%02d:%02d", m, s));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCompletion(MediaPlayer mp) {
|
||||
Log.d(TAG, "onCompletion");
|
||||
|
||||
super.onCompletion(mp);
|
||||
stopCounter();
|
||||
updateControls();
|
||||
if (currentState != State.ERROR)
|
||||
updateCounter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onError(MediaPlayer mp, int what, int extra) {
|
||||
boolean result = super.onError(mp, what, extra);
|
||||
stopCounter();
|
||||
updateControls();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
if (getCurrentState() == State.END) {
|
||||
Log.d(TAG, "onDetachedFromWindow END");
|
||||
stopCounter();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tryToPrepare() {
|
||||
Log.d(TAG, "tryToPrepare");
|
||||
super.tryToPrepare();
|
||||
|
||||
if (getCurrentState() == State.PREPARED || getCurrentState() == State.STARTED) {
|
||||
if (textElapsed != null && textTotal != null) {
|
||||
int total = getDuration();
|
||||
if (total > 0) {
|
||||
seekBar.setMax(total);
|
||||
seekBar.setProgress(0);
|
||||
|
||||
total = total / 1000;
|
||||
long s = total % 60;
|
||||
long m = (total / 60) % 60;
|
||||
long h = (total / (60 * 60)) % 24;
|
||||
if (h > 0) {
|
||||
textElapsed.setText("00:00:00");
|
||||
textTotal.setText(String.format(Locale.US, "%d:%02d:%02d", h, m, s));
|
||||
} else {
|
||||
textElapsed.setText("00:00");
|
||||
textTotal.setText(String.format(Locale.US, "%02d:%02d", m, s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (videoControlsView != null)
|
||||
videoControlsView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() throws IllegalStateException {
|
||||
Log.d(TAG, "start");
|
||||
|
||||
if (!isPlaying()) {
|
||||
super.start();
|
||||
startCounter();
|
||||
updateControls();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause() throws IllegalStateException {
|
||||
Log.d(TAG, "pause");
|
||||
|
||||
if (isPlaying()) {
|
||||
stopCounter();
|
||||
super.pause();
|
||||
updateControls();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
Log.d(TAG, "reset");
|
||||
|
||||
super.reset();
|
||||
|
||||
stopCounter();
|
||||
updateControls();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() throws IllegalStateException {
|
||||
Log.d(TAG, "stop");
|
||||
|
||||
super.stop();
|
||||
stopCounter();
|
||||
updateControls();
|
||||
}
|
||||
|
||||
protected void updateControls() {
|
||||
if (imgplay == null) return;
|
||||
|
||||
Drawable icon;
|
||||
if (getCurrentState() == State.STARTED) {
|
||||
icon = context.getResources().getDrawable(R.drawable.fvl_selector_pause);
|
||||
} else {
|
||||
icon = context.getResources().getDrawable(R.drawable.fvl_selector_play);
|
||||
}
|
||||
imgplay.setBackgroundDrawable(icon);
|
||||
}
|
||||
|
||||
public void hideControls() {
|
||||
Log.d(TAG, "hideControls");
|
||||
if (videoControlsView != null) {
|
||||
videoControlsView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void showControls() {
|
||||
Log.d(TAG, "showControls");
|
||||
if (videoControlsView != null) {
|
||||
videoControlsView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Onclick action
|
||||
* Controls play button and fullscreen button.
|
||||
*
|
||||
* @param v View defined in XML
|
||||
*/
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (v.getId() == R.id.vcv_img_play) {
|
||||
if (isPlaying()) {
|
||||
pause();
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
} else {
|
||||
setFullscreen(!isFullscreen());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SeekBar Listener
|
||||
*/
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
Log.d(TAG, "onProgressChanged " + progress);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartTrackingTouch(SeekBar seekBar) {
|
||||
stopCounter();
|
||||
Log.d(TAG, "onStartTrackingTouch");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStopTrackingTouch(SeekBar seekBar) {
|
||||
int progress = seekBar.getProgress();
|
||||
seekTo(progress);
|
||||
Log.d(TAG, "onStopTrackingTouch");
|
||||
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 153 B |
Before Width: | Height: | Size: 154 B |
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,27 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Copyright (C) 2010 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@android:id/background" android:drawable="@drawable/fvl_track" />
|
||||
<item android:id="@android:id/secondaryProgress">
|
||||
<scale android:scaleWidth="100%"
|
||||
android:drawable="@drawable/fvl_secondary" />
|
||||
</item>
|
||||
<item android:id="@android:id/progress">
|
||||
<scale android:scaleWidth="100%"
|
||||
android:drawable="@drawable/fvl_primary" />
|
||||
</item>
|
||||
</layer-list>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/fvl_fullscreen_reader" android:state_pressed="false" />
|
||||
<item android:drawable="@drawable/fvl_fullscreen_reader_white" android:state_pressed="true" />
|
||||
</selector>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/fvl_pause_reader" android:state_pressed="false" />
|
||||
<item android:drawable="@drawable/fvl_pause_reader_white" android:state_pressed="true" />
|
||||
</selector>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/fvl_play_reader" android:state_pressed="false" />
|
||||
<item android:drawable="@drawable/fvl_play_reader_white" android:state_pressed="true" />
|
||||
</selector>
|
|
@ -1,82 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
android:id="@+id/rel_videocontrols"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="0dp"
|
||||
android:background="#80cccccc">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/vcv_img_play"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginLeft="0dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_marginRight="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:background="@drawable/fvl_selector_play" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vcv_txt_elapsed"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toRightOf="@+id/vcv_img_play"
|
||||
android:text="00:00"
|
||||
android:textColor="@android:color/black" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/vcv_img_fullscreen"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginLeft="0dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_marginRight="0dp"
|
||||
android:layout_marginBottom="0dp"
|
||||
android:background="@drawable/fvl_selector_fullscreen" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vcv_txt_total"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginRight="5dp"
|
||||
android:layout_toLeftOf="@+id/vcv_img_fullscreen"
|
||||
android:text="00:00"
|
||||
android:textColor="@android:color/black" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/vcv_seekbar"
|
||||
style="@android:style/Widget.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="2dp"
|
||||
android:layout_marginStart="5dp"
|
||||
android:layout_marginLeft="5dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:layout_marginEnd="5dp"
|
||||
android:layout_marginRight="5dp"
|
||||
android:layout_marginBottom="5dp"
|
||||
android:layout_toLeftOf="@+id/vcv_txt_total"
|
||||
android:layout_toRightOf="@+id/vcv_txt_elapsed"
|
||||
android:indeterminateDrawable="@drawable/fvl_progress"
|
||||
android:maxHeight="13dp"
|
||||
android:minHeight="13dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:progressDrawable="@drawable/fvl_progress"
|
||||
android:thumb="@drawable/fvl_control_normal"
|
||||
android:thumbOffset="16dp" />
|
||||
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,6 +0,0 @@
|
|||
<resources>
|
||||
<string name="app_name">VideoLayout</string>
|
||||
|
||||
<string name="hello_world">Hello world!</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
</resources>
|
|
@ -7,7 +7,7 @@ repositories {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'rocks.xmpp:precis:1.0.0'
|
||||
implementation 'rocks.xmpp:precis:1.1.0'
|
||||
}
|
||||
|
||||
sourceCompatibility = "8"
|
||||
|
|
40
proguard-rules.pro
vendored
|
@ -12,6 +12,8 @@
|
|||
**[] $VALUES;
|
||||
public *;
|
||||
}
|
||||
-keep class com.squareup.okhttp.** { *; }
|
||||
-keep interface com.squareup.okhttp.** { *; }
|
||||
|
||||
-dontwarn org.bouncycastle.mail.**
|
||||
-dontwarn org.bouncycastle.x509.util.LDAPStoreHelper
|
||||
|
@ -22,6 +24,10 @@
|
|||
-dontwarn java.lang.**
|
||||
-dontwarn javax.lang.**
|
||||
|
||||
-keepclassmembers class de.pixart.messenger.http.services.** {
|
||||
!transient <fields>;
|
||||
}
|
||||
|
||||
# JSR 305 annotations are for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
|
@ -32,4 +38,36 @@
|
|||
-dontwarn org.codehaus.mojo.animal_sniffer.*
|
||||
|
||||
# OkHttp platform used only on JVM and when Conscrypt dependency is available.
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||
-dontwarn okhttp3.internal.platform.ConscryptPlatform
|
||||
|
||||
|
||||
|
||||
# Retrofit does reflection on generic parameters. InnerClasses is required to use Signature and
|
||||
# EnclosingMethod is required to use InnerClasses.
|
||||
-keepattributes Signature, InnerClasses, EnclosingMethod
|
||||
|
||||
# Retrofit does reflection on method and parameter annotations.
|
||||
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
|
||||
|
||||
# Retain service method parameters when optimizing.
|
||||
-keepclassmembers,allowshrinking,allowobfuscation interface * {
|
||||
@retrofit2.http.* <methods>;
|
||||
}
|
||||
|
||||
# Ignore annotation used for build tooling.
|
||||
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
|
||||
|
||||
# Ignore JSR 305 annotations for embedding nullability information.
|
||||
-dontwarn javax.annotation.**
|
||||
|
||||
# Guarded by a NoClassDefFoundError try/catch and only used when on the classpath.
|
||||
-dontwarn kotlin.Unit
|
||||
|
||||
# Top-level functions that can only be used by Kotlin.
|
||||
-dontwarn retrofit2.KotlinExtensions
|
||||
-dontwarn retrofit2.KotlinExtensions$*
|
||||
|
||||
# With R8 full mode, it sees no subtypes of Retrofit interfaces since they are created with a Proxy
|
||||
# and replaces all potential values with null. Explicitly keeping the interfaces prevents this.
|
||||
-if interface * { @retrofit2.http.* <methods>; }
|
||||
-keep,allowobfuscation interface <1>
|
|
@ -1,4 +1,3 @@
|
|||
include ':libs:android-transcoder'
|
||||
include ':libs:xmpp-addr'
|
||||
include ':libs:fullscreenvideoview'
|
||||
rootProject.name = 'PixArtMessenger'
|
||||
|
|
26
src/git/java/de/pixart/messenger/services/EmojiService.java
Normal file
|
@ -0,0 +1,26 @@
|
|||
package de.pixart.messenger.services;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.emoji.bundled.BundledEmojiCompatConfig;
|
||||
import androidx.emoji.text.EmojiCompat;
|
||||
|
||||
public class EmojiService {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public EmojiService(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public void init(boolean useBundledEmoji) {
|
||||
BundledEmojiCompatConfig config = new BundledEmojiCompatConfig(context);
|
||||
//On recent Androids we assume to have the latest emojis
|
||||
//there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible
|
||||
// a) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible)
|
||||
// b) can trigger a hardware rendering bug https://issuetracker.google.com/issues/67102093
|
||||
config.setReplaceAll(useBundledEmoji && Build.VERSION.SDK_INT < Build.VERSION_CODES.O);
|
||||
EmojiCompat.init(config);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package de.pixart.messenger.services;
|
||||
|
||||
import de.pixart.messenger.entities.Account;
|
||||
import de.pixart.messenger.entities.Conversation;
|
||||
|
||||
public class PushManagementService {
|
||||
|
||||
|
@ -10,7 +11,19 @@ public class PushManagementService {
|
|||
this.mXmppConnectionService = service;
|
||||
}
|
||||
|
||||
public void registerPushTokenOnServer(Account account) {
|
||||
void registerPushTokenOnServer(Account account) {
|
||||
//stub implementation. only affects playstore flavor
|
||||
}
|
||||
|
||||
void registerPushTokenOnServer(Conversation conversation) {
|
||||
//stub implementation. only affects playstore flavor
|
||||
}
|
||||
|
||||
void unregisterChannel(Account account, String hash) {
|
||||
//stub implementation. only affects playstore flavor
|
||||
}
|
||||
|
||||
void disablePushOnServer(Conversation conversation) {
|
||||
//stub implementation. only affects playstore flavor
|
||||
}
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
package de.pixart.messenger.ui.widget;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.text.emoji.widget.EmojiAppCompatEditText;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.emoji.widget.EmojiAppCompatEditText;
|
||||
|
||||
public class EmojiWrapperEditText extends EmojiAppCompatEditText {
|
||||
|
||||
public EmojiWrapperEditText(Context context) {
|
|
@ -0,0 +1,13 @@
|
|||
package de.pixart.messenger.utils;
|
||||
|
||||
import de.pixart.messenger.ui.MagicCreateActivity;
|
||||
import de.pixart.messenger.ui.WelcomeActivity;
|
||||
|
||||
public class InstallReferrerUtils {
|
||||
|
||||
public InstallReferrerUtils(WelcomeActivity welcomeActivity) {
|
||||
}
|
||||
|
||||
public static void markInstallReferrerExecuted(MagicCreateActivity magicCreateActivity) {
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
<uses-permission android:name="com.android.alarm.permission.SET_ALARM" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_PHONE_STATE"
|
||||
tools:node="remove" />
|
||||
|
@ -48,18 +49,19 @@
|
|||
android:required="false" />
|
||||
|
||||
<application
|
||||
android:name="android.support.multidex.MultiDexApplication"
|
||||
android:name="androidx.multidex.MultiDexApplication"
|
||||
android:allowBackup="false"
|
||||
android:appCategory="social"
|
||||
android:icon="@drawable/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:largeHeap="true"
|
||||
android:networkSecurityConfig="@xml/network_security_configuration"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/ConversationsTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
tools:replace="android:label, android:allowBackup"
|
||||
tools:targetApi="o">
|
||||
tools:targetApi="q">
|
||||
|
||||
<meta-data
|
||||
android:name="com.google.android.gms.car.application"
|
||||
|
@ -165,7 +167,22 @@
|
|||
<activity
|
||||
android:name=".ui.ImportBackupActivity"
|
||||
android:label="@string/restore_backup"
|
||||
android:launchMode="singleTask" />
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/vnd.conversations.backup" />
|
||||
<data android:scheme="content" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
||||
<data android:mimeType="application/vnd.conversations.backup" />
|
||||
<data android:scheme="file" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.SettingsActivity"
|
||||
android:label="@string/title_activity_settings">
|
||||
|
@ -212,7 +229,7 @@
|
|||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name=".ui.ContactDetailsActivity"
|
||||
android:label="@string/title_activity_contact_details"
|
||||
android:label="@string/contact_details"
|
||||
android:windowSoftInputMode="stateHidden" />
|
||||
<activity
|
||||
android:name=".ui.PublishProfilePictureActivity"
|
||||
|
@ -251,19 +268,24 @@
|
|||
<activity
|
||||
android:name=".ui.RecordingActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/ConversationsTheme.Dialog" />
|
||||
<activity
|
||||
android:name=".ui.ShareLocationActivity"
|
||||
android:label="@string/share_location" />
|
||||
android:label="@string/share_location"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.ShowLocationActivity"
|
||||
android:label="@string/show_location"></activity>
|
||||
android:label="@string/show_location"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.SearchActivity"
|
||||
android:label="@string/search_messages" />
|
||||
android:label="@string/search_messages"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.MediaViewerActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/ConversationsTheme.FullScreen" />
|
||||
<activity
|
||||
android:name=".ui.TrustKeysActivity"
|
||||
|
@ -272,14 +294,15 @@
|
|||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/title_activity_about"
|
||||
android:launchMode="singleTask"
|
||||
android:parentActivityName=".ui.SettingsActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ui.SettingsActivity" />
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.PREFERENCE" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".ui.SettingsActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.UpdaterActivity"
|
||||
|
@ -289,18 +312,21 @@
|
|||
android:theme="@style/ConversationsTheme" />
|
||||
<activity
|
||||
android:name=".ui.ShortcutActivity"
|
||||
android:label="@string/contact">
|
||||
android:label="@string/contact"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CREATE_SHORTCUT" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
<activity android:name=".ui.MemorizingActivity" />
|
||||
<activity
|
||||
android:name=".ui.MediaBrowserActivity"
|
||||
android:label="@string/media_browser" />
|
||||
android:label="@string/media_browser"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.EnterNameActivity"
|
||||
android:label="@string/enter_your_name"
|
||||
|
@ -311,10 +337,15 @@
|
|||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.MucUsersActivity"
|
||||
android:label="@string/group_chat_members" />
|
||||
android:label="@string/group_chat_members"
|
||||
android:launchMode="singleTask" />
|
||||
<activity
|
||||
android:name=".ui.ChannelDiscoveryActivity"
|
||||
android:label="@string/discover_channels" />
|
||||
android:label="@string/discover_channels"
|
||||
android:launchMode="singleTask" />
|
||||
<activity android:name=".ui.IntroActivity"
|
||||
android:launchMode="singleTask"
|
||||
android:label="@string/app_name" />
|
||||
|
||||
<service android:name=".services.ExportBackupService" />
|
||||
<service android:name=".services.ImportBackupService" />
|
||||
|
@ -327,7 +358,7 @@
|
|||
</service>
|
||||
|
||||
<provider
|
||||
android:name="android.support.v4.content.FileProvider"
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
|
|
7
src/main/assets/jquery.min.js
vendored
|
@ -25,6 +25,10 @@
|
|||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
/* Prevents IE11 from highlighting tiles in blue */
|
||||
.leaflet-tile::selection {
|
||||
background: transparent;
|
||||
}
|
||||
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
|
||||
.leaflet-safari .leaflet-tile {
|
||||
image-rendering: -webkit-optimize-contrast;
|
||||
|
@ -237,7 +241,8 @@
|
|||
|
||||
.leaflet-marker-icon.leaflet-interactive,
|
||||
.leaflet-image-layer.leaflet-interactive,
|
||||
.leaflet-pane > svg path.leaflet-interactive {
|
||||
.leaflet-pane > svg path.leaflet-interactive,
|
||||
svg.leaflet-image-layer.leaflet-interactive path {
|
||||
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
|
|
@ -13,15 +13,16 @@
|
|||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<style>
|
||||
#map{width:100%;height:100%;position:fixed;top:0px;left:0px;right:0px;bottom:0px}
|
||||
</style>
|
||||
|
||||
<div id='map'>
|
||||
</div>
|
||||
<div id='map'></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
var marker;
|
||||
|
||||
//get location from URL
|
||||
var getUrlParameter = function getUrlParameter(sParam) {
|
||||
var sPageURL = decodeURIComponent(window.location.search.substring(1)),
|
||||
sURLVariables = sPageURL.split('&'),
|
||||
|
@ -30,24 +31,33 @@
|
|||
|
||||
for (i = 0; i < sURLVariables.length; i++) {
|
||||
sParameterName = sURLVariables[i].split('=');
|
||||
|
||||
if (sParameterName[0] === sParam) {
|
||||
return sParameterName[1] === undefined ? true : sParameterName[1];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof getUrlParameter('lat') === 'undefined' && typeof getUrlParameter('lon') === 'undefined') {
|
||||
var map = L.map('map', {
|
||||
zoomControl: true,
|
||||
attributionControl: false
|
||||
}).fitWorld();
|
||||
} else {
|
||||
|
||||
// create map
|
||||
if (getUrlParameter('lat') != null && getUrlParameter('lon') != null) {
|
||||
var map = L.map('map', {
|
||||
zoomControl: true,
|
||||
attributionControl: true
|
||||
}).setView([getUrlParameter('lat'), getUrlParameter('lon')], 15);
|
||||
|
||||
// add marker
|
||||
if (!marker) {
|
||||
marker = L.marker([getUrlParameter('lat'), getUrlParameter('lon')]).addTo(map);
|
||||
marker.bindPopup((getUrlParameter('name')), {
|
||||
closeOnClick: false,
|
||||
closeButton: false,
|
||||
autoClose: false
|
||||
}).openPopup();
|
||||
}
|
||||
} else {
|
||||
var map = L.map('map', {
|
||||
zoomControl: true,
|
||||
attributionControl: true
|
||||
}).fitWorld();
|
||||
}
|
||||
|
||||
map.addLayer(new L.TileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
|
@ -55,16 +65,20 @@
|
|||
maxZoom: 19
|
||||
}));
|
||||
|
||||
if (typeof getUrlParameter('lat') !== 'undefined' && typeof getUrlParameter('lon') !== 'undefined' && typeof getUrlParameter('name') !== 'undefined') {
|
||||
var marker = L.marker([getUrlParameter('lat'), getUrlParameter('lon')]).addTo(map);
|
||||
marker.bindPopup(getUrlParameter('name'), {
|
||||
closeOnClick: false,
|
||||
closeButton: false,
|
||||
autoClose: false
|
||||
}).openPopup();
|
||||
} else if (typeof getUrlParameter('lat') !== 'undefined' && typeof getUrlParameter('lon') !== 'undefined' && typeof getUrlParameter('name') === 'undefined') {
|
||||
var marker = L.marker([getUrlParameter('lat'), getUrlParameter('lon')]).addTo(map);
|
||||
// Change view to coordinates
|
||||
function toCoordinates(latIntent, lonIntent, Name){
|
||||
map.setView([parseFloat(latIntent), parseFloat(lonIntent)], 15);
|
||||
if (!marker) {
|
||||
marker = L.marker([parseFloat(latIntent), parseFloat(lonIntent)]).addTo(map);
|
||||
marker.bindPopup(("..."), {
|
||||
closeOnClick: false,
|
||||
closeButton: false,
|
||||
autoClose: false
|
||||
}).openPopup();
|
||||
}
|
||||
marker.setLatLng([parseFloat(latIntent), parseFloat(lonIntent)]).update();
|
||||
marker.setPopupContent(Name);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
|
@ -39,8 +39,7 @@ public final class Config {
|
|||
return (ENCRYPTION_MASK & (ENCRYPTION_MASK - 1)) != 0;
|
||||
}
|
||||
|
||||
|
||||
public static final String LOGTAG = "Pix-Art_Messenger";
|
||||
public static final String LOGTAG = BuildConfig.LOGTAG;
|
||||
|
||||
public static final Jid BUG_REPORTS = Jid.of("bugs@pix-art.de");
|
||||
|
||||
|
@ -51,11 +50,13 @@ public final class Config {
|
|||
|
||||
public static final String XMPP_IP = null; //BuildConfig.XMPP_IP; // set to null means disable
|
||||
public static final Integer[] XMPP_Ports = null; //BuildConfig.XMPP_Ports; // set to null means disable
|
||||
public static final String DOMAIN_LOCK = null; //BuildConfig.DOMAIN_LOCK; //only allow account creation for this domain
|
||||
public static final String MAGIC_CREATE_DOMAIN = "blabber.im";
|
||||
public static final String DOMAIN_LOCK = BuildConfig.DOMAIN_LOCK; //only allow account creation for this domain
|
||||
public static final String MAGIC_CREATE_DOMAIN = BuildConfig.MAGIC_CREATE_DOMAIN; //"blabber.im";
|
||||
public static final String QUICKSY_DOMAIN = "quicksy.im";
|
||||
public static final String CHANNEL_DISCOVERY = "https://search.jabbercat.org";
|
||||
public static final String CHANNEL_DISCOVERY = "https://search.jabber.network";
|
||||
public static final String DEFAULT_INVIDIOUS_HOST = "invidio.us";
|
||||
public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
|
||||
public static final boolean SHOW_INTRO = BuildConfig.SHOW_INTRO;
|
||||
|
||||
public static final boolean USE_RANDOM_RESOURCE_ON_EVERY_BIND = false;
|
||||
|
||||
|
@ -109,15 +110,18 @@ public final class Config {
|
|||
public static final boolean REMOVE_BROKEN_DEVICES = false;
|
||||
public static final boolean OMEMO_PADDING = false;
|
||||
public static final boolean PUT_AUTH_TAG_INTO_KEY = true;
|
||||
public static final boolean TWELVE_BYTE_IV = true;
|
||||
|
||||
public static final int MAX_DISPLAY_MESSAGE_CHARS = 4096;
|
||||
public static final int MAX_STORAGE_MESSAGE_CHARS = 1024 * 1024 * 1024;
|
||||
public static final int MAX_STORAGE_MESSAGE_CHARS = 2 * 1024 * 1024; //2MB
|
||||
|
||||
public static final boolean ExportLogs = true; // automatically export logs
|
||||
public static final int ExportLogs_Hour = 4; //Time - hours: valid values from 0 to 23
|
||||
public static final int ExportLogs_Minute = 0; //Time - minutes: valid values from 0 to 59
|
||||
|
||||
public static final boolean USE_BOOKMARKS2 = false;
|
||||
public static final boolean DISABLE_PROXY_LOOKUP = false; //useful to debug ibb
|
||||
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
|
||||
public static final boolean DISABLE_HTTP_UPLOAD = false;
|
||||
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
|
||||
|
@ -130,11 +134,14 @@ public final class Config {
|
|||
public static final boolean ONLY_INTERNAL_STORAGE = false; //use internal storage instead of sdcard to save attachments
|
||||
|
||||
public static final boolean IGNORE_ID_REWRITE_IN_MUC = true;
|
||||
public static final boolean MUC_LEAVE_BEFORE_JOIN = true;
|
||||
|
||||
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 5;
|
||||
public static final boolean USE_LMC_VERSION_1_1 = true;
|
||||
|
||||
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY * 30;
|
||||
public static final int MAM_MAX_MESSAGES = 750;
|
||||
|
||||
public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE;
|
||||
public static final ChatState DEFAULT_CHAT_STATE = ChatState.ACTIVE;
|
||||
public static final int TYPING_TIMEOUT = 5;
|
||||
|
||||
public static final int EXPIRY_INTERVAL = 30 * 60 * 1000; // 30 minutes
|
||||
|
|
|
@ -23,7 +23,9 @@ public class JabberIdContact extends AbstractPhoneContact {
|
|||
super(cursor);
|
||||
try {
|
||||
this.jid = Jid.of(cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)));
|
||||
} catch (IllegalArgumentException | NullPointerException e) {
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (NullPointerException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,12 +39,17 @@ import de.pixart.messenger.ui.SettingsActivity;
|
|||
public class OmemoSetting {
|
||||
|
||||
private static boolean always = false;
|
||||
private static boolean never = false;
|
||||
private static int encryption = Message.ENCRYPTION_AXOLOTL;
|
||||
|
||||
public static boolean isAlways() {
|
||||
return always;
|
||||
}
|
||||
|
||||
public static boolean isNever() {
|
||||
return never;
|
||||
}
|
||||
|
||||
public static int getEncryption() {
|
||||
return encryption;
|
||||
}
|
||||
|
@ -54,14 +59,22 @@ public class OmemoSetting {
|
|||
switch (value) {
|
||||
case "always":
|
||||
always = true;
|
||||
never = false;
|
||||
encryption = Message.ENCRYPTION_AXOLOTL;
|
||||
break;
|
||||
case "default_on":
|
||||
always = false;
|
||||
never = false;
|
||||
encryption = Message.ENCRYPTION_AXOLOTL;
|
||||
break;
|
||||
case "always_off":
|
||||
always = false;
|
||||
never = true;
|
||||
encryption = Message.ENCRYPTION_NONE;
|
||||
break;
|
||||
default:
|
||||
always = false;
|
||||
never = false;
|
||||
encryption = Message.ENCRYPTION_NONE;
|
||||
break;
|
||||
|
||||
|
|
|
@ -95,7 +95,9 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
|
|||
this.account.setKey("otr_p", privateKeySpec.getP().toString(16));
|
||||
this.account.setKey("otr_q", privateKeySpec.getQ().toString(16));
|
||||
this.account.setKey("otr_y", publicKeySpec.getY().toString(16));
|
||||
} catch (final NoSuchAlgorithmException | InvalidKeySpecException e) {
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
e.printStackTrace();
|
||||
} catch (final InvalidKeySpecException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
|
@ -186,7 +188,7 @@ public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
|
|||
try {
|
||||
Jid jid = OtrJidHelper.fromSessionID(session);
|
||||
Conversation conversation = mXmppConnectionService.find(account, jid);
|
||||
if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
|
||||
if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHAT_STATE)) {
|
||||
if (mXmppConnectionService.sendChatStates()) {
|
||||
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
|
||||
}
|
||||
|
|
|
@ -201,6 +201,9 @@ public class PgpDecryptionService {
|
|||
if (fixedFile.getParentFile().mkdirs()) {
|
||||
Log.d(Config.LOGTAG, "created parent directories for " + fixedFile.getAbsolutePath());
|
||||
}
|
||||
synchronized (mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION) {
|
||||
mXmppConnectionService.FILENAMES_TO_IGNORE_DELETION.add(outputFile.getAbsolutePath());
|
||||
}
|
||||
if (outputFile.renameTo(fixedFile)) {
|
||||
Log.d(Config.LOGTAG, "renamed " + outputFile.getAbsolutePath() + " to " + fixedFile.getAbsolutePath());
|
||||
message.setRelativeFilePath(path);
|
||||
|
|
|
@ -2,9 +2,10 @@ package de.pixart.messenger.crypto;
|
|||
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Intent;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.openintents.openpgp.OpenPgpError;
|
||||
import org.openintents.openpgp.OpenPgpSignatureResult;
|
||||
import org.openintents.openpgp.util.OpenPgpApi;
|
||||
|
@ -92,7 +93,7 @@ public class PgpEngine {
|
|||
}
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
|
@ -131,7 +132,7 @@ public class PgpEngine {
|
|||
callback.success(message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), message);
|
||||
break;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(conversation.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
|
@ -198,7 +199,7 @@ public class PgpEngine {
|
|||
callback.success(account);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account);
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), account);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(account, result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
|
@ -247,7 +248,7 @@ public class PgpEngine {
|
|||
callback.success(signatureBuilder.toString());
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status);
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), status);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
|
||||
|
@ -274,7 +275,7 @@ public class PgpEngine {
|
|||
callback.success(contact);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
|
||||
callback.userInputRequried(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact);
|
||||
callback.userInputRequired(result.getParcelableExtra(OpenPgpApi.RESULT_INTENT), contact);
|
||||
return;
|
||||
case OpenPgpApi.RESULT_CODE_ERROR:
|
||||
logError(contact.getAccount(), result.getParcelableExtra(OpenPgpApi.RESULT_ERROR));
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.security.cert.X509Certificate;
|
|||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.net.ssl.SSLSession;
|
||||
|
||||
|
@ -80,14 +81,14 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
|
|||
break;
|
||||
}
|
||||
Log.d(LOGTAG, "comparing " + needle.substring(i) + " and " + entry.substring(1));
|
||||
if (needle.substring(i).equals(entry.substring(1))) {
|
||||
if (needle.substring(i).equalsIgnoreCase(entry.substring(1))) {
|
||||
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
|
||||
return true;
|
||||
}
|
||||
offset = i + 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.equals(needle)) {
|
||||
if (entry.equalsIgnoreCase(needle)) {
|
||||
Log.d(LOGTAG, "domain " + needle + " matched " + entry);
|
||||
return true;
|
||||
}
|
||||
|
@ -117,25 +118,25 @@ public class XmppDomainVerifier implements DomainHostnameVerifier {
|
|||
List<String> domains = new ArrayList<>();
|
||||
if (alternativeNames != null) {
|
||||
for (List<?> san : alternativeNames) {
|
||||
Integer type = (Integer) san.get(0);
|
||||
final Integer type = (Integer) san.get(0);
|
||||
if (type == 0) {
|
||||
Pair<String, String> otherName = parseOtherName((byte[]) san.get(1));
|
||||
if (otherName != null) {
|
||||
final Pair<String, String> otherName = parseOtherName((byte[]) san.get(1));
|
||||
if (otherName != null && otherName.first != null && otherName.second != null) {
|
||||
switch (otherName.first) {
|
||||
case SRV_NAME:
|
||||
srvNames.add(otherName.second);
|
||||
srvNames.add(otherName.second.toLowerCase(Locale.US));
|
||||
break;
|
||||
case XMPP_ADDR:
|
||||
xmppAddrs.add(otherName.second);
|
||||
xmppAddrs.add(otherName.second.toLowerCase(Locale.US));
|
||||
break;
|
||||
default:
|
||||
Log.d(LOGTAG, "oid: " + otherName.first + " value: " + otherName.second);
|
||||
}
|
||||
}
|
||||
} else if (type == 2) {
|
||||
Object value = san.get(1);
|
||||
final Object value = san.get(1);
|
||||
if (value instanceof String) {
|
||||
domains.add((String) value);
|
||||
domains.add(((String) value).toLowerCase(Locale.US));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,12 @@ package de.pixart.messenger.crypto.axolotl;
|
|||
|
||||
import android.os.Bundle;
|
||||
import android.security.KeyChain;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.IdentityKeyPair;
|
||||
|
@ -66,8 +67,8 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
|
||||
public static final String LOGPREFIX = "AxolotlService";
|
||||
|
||||
public static final int NUM_KEYS_TO_PUBLISH = 100;
|
||||
public static final int publishTriesThreshold = 3;
|
||||
private static final int NUM_KEYS_TO_PUBLISH = 100;
|
||||
private static final int publishTriesThreshold = 3;
|
||||
|
||||
private final Account account;
|
||||
private final XmppConnectionService mXmppConnectionService;
|
||||
|
@ -79,16 +80,37 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
private final Map<Jid, Boolean> fetchDeviceListStatus = new HashMap<>();
|
||||
private final HashMap<Jid, List<OnDeviceIdsFetched>> fetchDeviceIdsMap = new HashMap<>();
|
||||
private final SerialSingleThreadExecutor executor;
|
||||
private final Set<SignalProtocolAddress> healingAttempts = new HashSet<>();
|
||||
private final HashSet<Integer> cleanedOwnDeviceIds = new HashSet<>();
|
||||
private final Set<Integer> PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>();
|
||||
private int numPublishTriesOnEmptyPep = 0;
|
||||
private boolean pepBroken = false;
|
||||
private final Set<SignalProtocolAddress> healingAttempts = new HashSet<>();
|
||||
private int lastDeviceListNotificationHash = 0;
|
||||
private final HashSet<Integer> cleanedOwnDeviceIds = new HashSet<>();
|
||||
private Set<XmppAxolotlSession> postponedSessions = new HashSet<>(); //sessions stored here will receive after mam catchup treatment
|
||||
private Set<SignalProtocolAddress> postponedHealing = new HashSet<>(); //addresses stored here will need a healing notification after mam catchup
|
||||
|
||||
private AtomicBoolean changeAccessMode = new AtomicBoolean(false);
|
||||
|
||||
public AxolotlService(Account account, XmppConnectionService connectionService) {
|
||||
if (account == null || connectionService == null) {
|
||||
throw new IllegalArgumentException("account and service cannot be null");
|
||||
}
|
||||
if (Security.getProvider("BC") == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
this.mXmppConnectionService = connectionService;
|
||||
this.account = account;
|
||||
this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
|
||||
this.deviceIds = new HashMap<>();
|
||||
this.messageCache = new HashMap<>();
|
||||
this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
|
||||
this.fetchStatusMap = new FetchStatusMap();
|
||||
this.executor = new SerialSingleThreadExecutor("Axolotl");
|
||||
}
|
||||
|
||||
public static String getLogprefix(Account account) {
|
||||
return LOGPREFIX + " (" + account.getJid().asBareJid().toString() + "): ";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAdvancedStreamFeaturesAvailable(Account account) {
|
||||
if (Config.supportOmemo()
|
||||
|
@ -145,172 +167,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return false;
|
||||
}
|
||||
|
||||
private static class AxolotlAddressMap<T> {
|
||||
protected Map<String, Map<Integer, T>> map;
|
||||
protected final Object MAP_LOCK = new Object();
|
||||
|
||||
public AxolotlAddressMap() {
|
||||
this.map = new HashMap<>();
|
||||
}
|
||||
|
||||
public void put(SignalProtocolAddress address, T value) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, T> devices = map.get(address.getName());
|
||||
if (devices == null) {
|
||||
devices = new HashMap<>();
|
||||
map.put(address.getName(), devices);
|
||||
}
|
||||
devices.put(address.getDeviceId(), value);
|
||||
}
|
||||
}
|
||||
|
||||
public T get(SignalProtocolAddress address) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, T> devices = map.get(address.getName());
|
||||
if (devices == null) {
|
||||
return null;
|
||||
}
|
||||
return devices.get(address.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Integer, T> getAll(String name) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, T> devices = map.get(name);
|
||||
if (devices == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAny(SignalProtocolAddress address) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, T> devices = map.get(address.getName());
|
||||
return devices != null && !devices.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
|
||||
private final XmppConnectionService xmppConnectionService;
|
||||
private final Account account;
|
||||
|
||||
public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
|
||||
super();
|
||||
this.xmppConnectionService = service;
|
||||
this.account = account;
|
||||
this.fillMap(store);
|
||||
}
|
||||
|
||||
public Set<Jid> findCounterpartsForSourceId(Integer sid) {
|
||||
Set<Jid> candidates = new HashSet<>();
|
||||
synchronized (MAP_LOCK) {
|
||||
for (Map.Entry<String, Map<Integer, XmppAxolotlSession>> entry : map.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (entry.getValue().containsKey(sid)) {
|
||||
candidates.add(Jid.of(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
|
||||
for (Integer deviceId : deviceIds) {
|
||||
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId);
|
||||
IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
|
||||
if (Config.X509_VERIFICATION) {
|
||||
X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()));
|
||||
if (certificate != null) {
|
||||
Bundle information = CryptoHelper.extractCertificateInformation(certificate);
|
||||
try {
|
||||
final String cn = information.getString("subject_cn");
|
||||
final Jid jid = Jid.of(bareJid);
|
||||
Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn);
|
||||
account.getRoster().getContact(jid).setCommonName(cn);
|
||||
} catch (final IllegalArgumentException ignored) {
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillMap(SQLiteAxolotlStore store) {
|
||||
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString());
|
||||
putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store);
|
||||
for (String address : store.getKnownAddresses()) {
|
||||
deviceIds = store.getSubDeviceSessions(address);
|
||||
putDevicesForJid(address, deviceIds, store);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(SignalProtocolAddress address, XmppAxolotlSession value) {
|
||||
super.put(address, value);
|
||||
value.setNotFresh();
|
||||
}
|
||||
|
||||
public void put(XmppAxolotlSession session) {
|
||||
this.put(session.getRemoteAddress(), session);
|
||||
}
|
||||
}
|
||||
|
||||
public enum FetchStatus {
|
||||
PENDING,
|
||||
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.asBareJid().toString());
|
||||
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.asBareJid() + "(" + entry.getKey() + ")");
|
||||
entry.setValue(FetchStatus.TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getLogprefix(Account account) {
|
||||
return LOGPREFIX + " (" + account.getJid().asBareJid().toString() + "): ";
|
||||
}
|
||||
|
||||
public AxolotlService(Account account, XmppConnectionService connectionService) {
|
||||
if (account == null || connectionService == null) {
|
||||
throw new IllegalArgumentException("account and service cannot be null");
|
||||
}
|
||||
if (Security.getProvider("BC") == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
this.mXmppConnectionService = connectionService;
|
||||
this.account = account;
|
||||
this.axolotlStore = new SQLiteAxolotlStore(this.account, this.mXmppConnectionService);
|
||||
this.deviceIds = new HashMap<>();
|
||||
this.messageCache = new HashMap<>();
|
||||
this.sessions = new SessionMap(mXmppConnectionService, axolotlStore, account);
|
||||
this.fetchStatusMap = new FetchStatusMap();
|
||||
this.executor = new SerialSingleThreadExecutor("Axolotl");
|
||||
}
|
||||
|
||||
public String getOwnFingerprint() {
|
||||
return CryptoHelper.bytesToHex(axolotlStore.getIdentityKeyPair().getPublicKey().serialize());
|
||||
}
|
||||
|
@ -359,7 +215,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return s;
|
||||
}
|
||||
|
||||
|
||||
public Collection<XmppAxolotlSession> findSessionsForContact(Contact contact) {
|
||||
SignalProtocolAddress contactAddress = getAddressForJid(contact.getJid());
|
||||
ArrayList<XmppAxolotlSession> s = new ArrayList<>(this.sessions.getAll(contactAddress.getName()).values());
|
||||
|
@ -797,7 +652,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
final boolean wipe,
|
||||
final boolean firstAttempt) {
|
||||
final Bundle publishOptions = account.getXmppConnection().getFeatures().pepPublishOptions() ? PublishOptions.openAccess() : null;
|
||||
IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
|
||||
final IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
|
||||
signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
|
||||
preKeyRecords, getOwnDeviceId(), publishOptions);
|
||||
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing...");
|
||||
|
@ -839,6 +694,14 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
});
|
||||
}
|
||||
|
||||
public void deleteOmemoIdentity() {
|
||||
final String node = AxolotlService.PEP_BUNDLES + ":" + getOwnDeviceId();
|
||||
final IqPacket deleteBundleNode = mXmppConnectionService.getIqGenerator().deleteNode(node);
|
||||
mXmppConnectionService.sendIqPacket(account, deleteBundleNode, null);
|
||||
final Set<Integer> ownDeviceIds = getOwnDeviceIds();
|
||||
publishDeviceIdsAndRefineAccessModel(ownDeviceIds == null ? Collections.emptySet() : ownDeviceIds);
|
||||
}
|
||||
|
||||
public List<Jid> getCryptoTargets(Conversation conversation) {
|
||||
final List<Jid> jids;
|
||||
if (conversation.getMode() == Conversation.MODE_SINGLE) {
|
||||
|
@ -916,8 +779,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
private final Set<Integer> PREVIOUSLY_REMOVED_FROM_ANNOUNCEMENT = new HashSet<>();
|
||||
|
||||
private void finishBuildingSessionsFromPEP(final SignalProtocolAddress address) {
|
||||
SignalProtocolAddress ownAddress = new SignalProtocolAddress(account.getJid().asBareJid().toString(), 0);
|
||||
Map<Integer, FetchStatus> own = fetchStatusMap.getAll(ownAddress.getName());
|
||||
|
@ -955,14 +816,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return !hasAny(jid) && (!deviceIds.containsKey(jid) || deviceIds.get(jid).isEmpty());
|
||||
}
|
||||
|
||||
public interface OnDeviceIdsFetched {
|
||||
void fetched(Jid jid, Set<Integer> deviceIds);
|
||||
}
|
||||
|
||||
public interface OnMultipleDeviceIdFetched {
|
||||
void fetched();
|
||||
}
|
||||
|
||||
public void fetchDeviceIds(final Jid jid) {
|
||||
fetchDeviceIds(jid, null);
|
||||
}
|
||||
|
@ -1039,12 +892,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
interface OnSessionBuildFromPep {
|
||||
void onSessionBuildSuccessful();
|
||||
|
||||
void onSessionBuildFailed();
|
||||
}
|
||||
|
||||
private void buildSessionFromPEP(final SignalProtocolAddress address) {
|
||||
buildSessionFromPEP(address, null);
|
||||
}
|
||||
|
@ -1392,7 +1239,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
private XmppAxolotlSession getReceivingSession(SignalProtocolAddress senderAddress) {
|
||||
XmppAxolotlSession session = sessions.get(senderAddress);
|
||||
if (session == null) {
|
||||
//Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Account: " + account.getJid() + " No axolotl session found while parsing received message " + message);
|
||||
session = recreateUncachedSession(senderAddress);
|
||||
if (session == null) {
|
||||
session = new XmppAxolotlSession(account, axolotlStore, senderAddress);
|
||||
|
@ -1401,7 +1247,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return session;
|
||||
}
|
||||
|
||||
public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException {
|
||||
public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message, boolean postponePreKeyMessageHandling) throws NotEncryptedForThisDeviceException, BrokenSessionException, OutdatedSenderException {
|
||||
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
|
||||
|
||||
XmppAxolotlSession session = getReceivingSession(message);
|
||||
|
@ -1420,6 +1266,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
} catch (final BrokenSessionException e) {
|
||||
throw e;
|
||||
} catch (final OutdatedSenderException e) {
|
||||
Log.e(Config.LOGTAG, account.getJid().asBareJid() + ": " + e.getMessage());
|
||||
throw e;
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from " + message.getFrom(), e);
|
||||
}
|
||||
|
@ -1469,7 +1318,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": nothing to flush. Not republishing key");
|
||||
}
|
||||
completeSession(session);
|
||||
if (trustedOrPreviouslyResponded(session)) {
|
||||
completeSession(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1479,23 +1330,43 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
publishBundlesIfNeeded(false, false);
|
||||
}
|
||||
}
|
||||
Iterator<XmppAxolotlSession> iterator = postponedSessions.iterator();
|
||||
final Iterator<XmppAxolotlSession> iterator = postponedSessions.iterator();
|
||||
while (iterator.hasNext()) {
|
||||
completeSession(iterator.next());
|
||||
final XmppAxolotlSession session = iterator.next();
|
||||
if (trustedOrPreviouslyResponded(session)) {
|
||||
completeSession(session);
|
||||
}
|
||||
iterator.remove();
|
||||
}
|
||||
Iterator<SignalProtocolAddress> postponedHealingAttemptsIterator = postponedHealing.iterator();
|
||||
final Iterator<SignalProtocolAddress> postponedHealingAttemptsIterator = postponedHealing.iterator();
|
||||
while (postponedHealingAttemptsIterator.hasNext()) {
|
||||
notifyRequiresHealing(postponedHealingAttemptsIterator.next());
|
||||
postponedHealingAttemptsIterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean trustedOrPreviouslyResponded(XmppAxolotlSession session) {
|
||||
try {
|
||||
return trustedOrPreviouslyResponded(Jid.of(session.getRemoteAddress().getName()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean trustedOrPreviouslyResponded(Jid jid) {
|
||||
final Contact contact = account.getRoster().getContact(jid);
|
||||
if (contact.showInRoster() || contact.isSelf()) {
|
||||
return true;
|
||||
}
|
||||
final Conversation conversation = mXmppConnectionService.find(account, jid);
|
||||
return conversation != null && conversation.sentMessagesCount() > 0;
|
||||
}
|
||||
|
||||
private void completeSession(XmppAxolotlSession session) {
|
||||
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(account.getJid().asBareJid(), getOwnDeviceId());
|
||||
axolotlMessage.addDevice(session, true);
|
||||
try {
|
||||
Jid jid = Jid.of(session.getRemoteAddress().getName());
|
||||
final Jid jid = Jid.of(session.getRemoteAddress().getName());
|
||||
MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateKeyTransportMessage(jid, axolotlMessage);
|
||||
mXmppConnectionService.sendMessagePacket(account, packet);
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
@ -1503,11 +1374,9 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message, final boolean postponePreKeyMessageHandling) {
|
||||
XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
|
||||
|
||||
XmppAxolotlSession session = getReceivingSession(message);
|
||||
final XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage;
|
||||
final XmppAxolotlSession session = getReceivingSession(message);
|
||||
try {
|
||||
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
|
||||
Integer preKeyId = session.getPreKeyIdAndReset();
|
||||
|
@ -1516,7 +1385,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
} catch (CryptoFailedException e) {
|
||||
Log.d(Config.LOGTAG, "could not decrypt keyTransport message " + e.getMessage());
|
||||
keyTransportMessage = null;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (session.isFresh() && keyTransportMessage != null) {
|
||||
|
@ -1527,7 +1396,6 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
|
||||
private void putFreshSession(XmppAxolotlSession session) {
|
||||
Log.d(Config.LOGTAG, "put fresh session");
|
||||
sessions.put(session);
|
||||
if (Config.X509_VERIFICATION) {
|
||||
if (session.getIdentityKey() != null) {
|
||||
|
@ -1537,4 +1405,164 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum FetchStatus {
|
||||
PENDING,
|
||||
SUCCESS,
|
||||
SUCCESS_VERIFIED,
|
||||
TIMEOUT,
|
||||
SUCCESS_TRUSTED,
|
||||
ERROR
|
||||
}
|
||||
|
||||
public interface OnDeviceIdsFetched {
|
||||
void fetched(Jid jid, Set<Integer> deviceIds);
|
||||
}
|
||||
|
||||
|
||||
public interface OnMultipleDeviceIdFetched {
|
||||
void fetched();
|
||||
}
|
||||
|
||||
interface OnSessionBuildFromPep {
|
||||
void onSessionBuildSuccessful();
|
||||
|
||||
void onSessionBuildFailed();
|
||||
}
|
||||
|
||||
private static class AxolotlAddressMap<T> {
|
||||
protected final Object MAP_LOCK = new Object();
|
||||
protected Map<String, Map<Integer, T>> map;
|
||||
|
||||
public AxolotlAddressMap() {
|
||||
this.map = new HashMap<>();
|
||||
}
|
||||
|
||||
public void put(SignalProtocolAddress address, T value) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, T> devices = map.get(address.getName());
|
||||
if (devices == null) {
|
||||
devices = new HashMap<>();
|
||||
map.put(address.getName(), devices);
|
||||
}
|
||||
devices.put(address.getDeviceId(), value);
|
||||
}
|
||||
}
|
||||
|
||||
public T get(SignalProtocolAddress address) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, T> devices = map.get(address.getName());
|
||||
if (devices == null) {
|
||||
return null;
|
||||
}
|
||||
return devices.get(address.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Integer, T> getAll(String name) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, T> devices = map.get(name);
|
||||
if (devices == null) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return devices;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasAny(SignalProtocolAddress address) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, T> devices = map.get(address.getName());
|
||||
return devices != null && !devices.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class SessionMap extends AxolotlAddressMap<XmppAxolotlSession> {
|
||||
private final XmppConnectionService xmppConnectionService;
|
||||
private final Account account;
|
||||
|
||||
public SessionMap(XmppConnectionService service, SQLiteAxolotlStore store, Account account) {
|
||||
super();
|
||||
this.xmppConnectionService = service;
|
||||
this.account = account;
|
||||
this.fillMap(store);
|
||||
}
|
||||
|
||||
public Set<Jid> findCounterpartsForSourceId(Integer sid) {
|
||||
Set<Jid> candidates = new HashSet<>();
|
||||
synchronized (MAP_LOCK) {
|
||||
for (Map.Entry<String, Map<Integer, XmppAxolotlSession>> entry : map.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (entry.getValue().containsKey(sid)) {
|
||||
candidates.add(Jid.of(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
return candidates;
|
||||
}
|
||||
|
||||
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
|
||||
for (Integer deviceId : deviceIds) {
|
||||
SignalProtocolAddress axolotlAddress = new SignalProtocolAddress(bareJid, deviceId);
|
||||
IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey();
|
||||
if (Config.X509_VERIFICATION) {
|
||||
X509Certificate certificate = store.getFingerprintCertificate(CryptoHelper.bytesToHex(identityKey.getPublicKey().serialize()));
|
||||
if (certificate != null) {
|
||||
Bundle information = CryptoHelper.extractCertificateInformation(certificate);
|
||||
try {
|
||||
final String cn = information.getString("subject_cn");
|
||||
final Jid jid = Jid.of(bareJid);
|
||||
Log.d(Config.LOGTAG, "setting common name for " + jid + " to " + cn);
|
||||
account.getRoster().getContact(jid).setCommonName(cn);
|
||||
} catch (final IllegalArgumentException ignored) {
|
||||
//ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey));
|
||||
}
|
||||
}
|
||||
|
||||
private void fillMap(SQLiteAxolotlStore store) {
|
||||
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().asBareJid().toString());
|
||||
putDevicesForJid(account.getJid().asBareJid().toString(), deviceIds, store);
|
||||
for (String address : store.getKnownAddresses()) {
|
||||
deviceIds = store.getSubDeviceSessions(address);
|
||||
putDevicesForJid(address, deviceIds, store);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(SignalProtocolAddress address, XmppAxolotlSession value) {
|
||||
super.put(address, value);
|
||||
value.setNotFresh();
|
||||
}
|
||||
|
||||
public void put(XmppAxolotlSession session) {
|
||||
this.put(session.getRemoteAddress(), session);
|
||||
}
|
||||
}
|
||||
|
||||
private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
|
||||
|
||||
public void clearErrorFor(Jid jid) {
|
||||
synchronized (MAP_LOCK) {
|
||||
Map<Integer, FetchStatus> devices = this.map.get(jid.asBareJid().toString());
|
||||
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.asBareJid() + "(" + entry.getKey() + ")");
|
||||
entry.setValue(FetchStatus.TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package de.pixart.messenger.crypto.axolotl;
|
||||
|
||||
public class OutdatedSenderException extends CryptoFailedException {
|
||||
|
||||
public OutdatedSenderException(final String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
|
@ -37,69 +37,13 @@ public class XmppAxolotlMessage {
|
|||
private static final String KEYTYPE = "AES";
|
||||
private static final String CIPHERMODE = "AES/GCM/NoPadding";
|
||||
private static final String PROVIDER = "BC";
|
||||
|
||||
private final List<XmppAxolotlSession.AxolotlKey> keys;
|
||||
private final Jid from;
|
||||
private final int sourceDeviceId;
|
||||
private byte[] innerKey;
|
||||
private byte[] ciphertext = null;
|
||||
private byte[] authtagPlusInnerKey = null;
|
||||
private byte[] iv = null;
|
||||
private final List<XmppAxolotlSession.AxolotlKey> keys;
|
||||
private final Jid from;
|
||||
private final int sourceDeviceId;
|
||||
|
||||
public static class XmppAxolotlPlaintextMessage {
|
||||
private final String plaintext;
|
||||
private final String fingerprint;
|
||||
|
||||
XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) {
|
||||
this.plaintext = plaintext;
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
public String getPlaintext() {
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
|
||||
public String getFingerprint() {
|
||||
return fingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
public static class XmppAxolotlKeyTransportMessage {
|
||||
private final String fingerprint;
|
||||
private final byte[] key;
|
||||
private final byte[] iv;
|
||||
|
||||
XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) {
|
||||
this.fingerprint = fingerprint;
|
||||
this.key = key;
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
}
|
||||
|
||||
public static int parseSourceId(final Element axolotlMessage) throws IllegalArgumentException {
|
||||
final Element header = axolotlMessage.findChild(HEADER);
|
||||
if (header == null) {
|
||||
throw new IllegalArgumentException("No header found");
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(header.getAttribute(SOURCEID));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("invalid source id");
|
||||
}
|
||||
}
|
||||
|
||||
private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
|
||||
this.from = from;
|
||||
|
@ -134,7 +78,7 @@ public class XmppAxolotlMessage {
|
|||
break;
|
||||
}
|
||||
}
|
||||
Element payloadElement = axolotlMessage.findChild(PAYLOAD);
|
||||
final Element payloadElement = axolotlMessage.findChildEnsureSingle(PAYLOAD, AxolotlService.PEP_PREFIX);
|
||||
if (payloadElement != null) {
|
||||
ciphertext = Base64.decode(payloadElement.getContent().trim(), Base64.DEFAULT);
|
||||
}
|
||||
|
@ -148,6 +92,18 @@ public class XmppAxolotlMessage {
|
|||
this.innerKey = generateKey();
|
||||
}
|
||||
|
||||
public static int parseSourceId(final Element axolotlMessage) throws IllegalArgumentException {
|
||||
final Element header = axolotlMessage.findChild(HEADER);
|
||||
if (header == null) {
|
||||
throw new IllegalArgumentException("No header found");
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(header.getAttribute(SOURCEID));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new IllegalArgumentException("invalid source id");
|
||||
}
|
||||
}
|
||||
|
||||
public static XmppAxolotlMessage fromElement(Element element, Jid from) {
|
||||
return new XmppAxolotlMessage(element, from);
|
||||
}
|
||||
|
@ -164,12 +120,28 @@ public class XmppAxolotlMessage {
|
|||
}
|
||||
|
||||
private static byte[] generateIv() {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] iv = new byte[16];
|
||||
final SecureRandom random = new SecureRandom();
|
||||
final byte[] iv = new byte[12];
|
||||
random.nextBytes(iv);
|
||||
return iv;
|
||||
}
|
||||
|
||||
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 boolean hasPayload() {
|
||||
return ciphertext != null;
|
||||
}
|
||||
|
@ -196,22 +168,6 @@ 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;
|
||||
}
|
||||
|
@ -287,19 +243,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;
|
||||
if (key.length < 32) {
|
||||
throw new OutdatedSenderException("Key did not contain auth tag. Sender needs to update their OMEMO client");
|
||||
}
|
||||
final int authTagLength = key.length - 16;
|
||||
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 = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(iv);
|
||||
|
||||
|
@ -316,4 +272,47 @@ public class XmppAxolotlMessage {
|
|||
}
|
||||
return plaintextMessage;
|
||||
}
|
||||
|
||||
public static class XmppAxolotlPlaintextMessage {
|
||||
private final String plaintext;
|
||||
private final String fingerprint;
|
||||
|
||||
XmppAxolotlPlaintextMessage(String plaintext, String fingerprint) {
|
||||
this.plaintext = plaintext;
|
||||
this.fingerprint = fingerprint;
|
||||
}
|
||||
|
||||
public String getPlaintext() {
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
|
||||
public String getFingerprint() {
|
||||
return fingerprint;
|
||||
}
|
||||
}
|
||||
|
||||
public static class XmppAxolotlKeyTransportMessage {
|
||||
private final String fingerprint;
|
||||
private final byte[] key;
|
||||
private final byte[] iv;
|
||||
|
||||
XmppAxolotlKeyTransportMessage(String fingerprint, byte[] key, byte[] iv) {
|
||||
this.fingerprint = fingerprint;
|
||||
this.key = key;
|
||||
this.iv = iv;
|
||||
}
|
||||
|
||||
public String getFingerprint() {
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
public byte[] getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public byte[] getIv() {
|
||||
return iv;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
package de.pixart.messenger.crypto.axolotl;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.whispersystems.libsignal.DuplicateMessageException;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.InvalidKeyException;
|
||||
|
@ -115,7 +116,13 @@ public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> {
|
|||
SignalMessage signalMessage = new SignalMessage(encryptedKey.key);
|
||||
try {
|
||||
plaintext = cipher.decrypt(signalMessage);
|
||||
} catch (InvalidMessageException | NoSessionException e) {
|
||||
} catch (InvalidMessageException e) {
|
||||
if (iterator.hasNext()) {
|
||||
Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring crypto exception because possible keys left to try", e);
|
||||
continue;
|
||||
}
|
||||
throw new BrokenSessionException(this.remoteAddress, e);
|
||||
} catch (NoSessionException e) {
|
||||
if (iterator.hasNext()) {
|
||||
Log.w(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring crypto exception because possible keys left to try", e);
|
||||
continue;
|
||||
|
|
|
@ -43,7 +43,9 @@ abstract class ScramMechanism extends SaslMechanism {
|
|||
clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES);
|
||||
|
||||
return new KeyPair(clientKey, serverKey);
|
||||
} catch (final InvalidKeyException | NumberFormatException e) {
|
||||
} catch (final InvalidKeyException e) {
|
||||
return null;
|
||||
} catch (final NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,11 +16,12 @@ import java.security.PublicKey;
|
|||
import java.security.interfaces.DSAPublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import de.pixart.messenger.Config;
|
||||
|
@ -55,6 +56,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
public static final String RESOURCE = "resource";
|
||||
|
||||
public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
|
||||
public static final String PRE_AUTH_REGISTRATION_TOKEN = "pre_auth_registration";
|
||||
|
||||
public static final int OPTION_USETLS = 0;
|
||||
public static final int OPTION_DISABLED = 1;
|
||||
|
@ -64,15 +66,18 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
public static final int OPTION_REQUIRES_ACCESS_MODE_CHANGE = 5;
|
||||
public static final int OPTION_LOGGED_IN_SUCCESSFULLY = 6;
|
||||
public static final int OPTION_HTTP_UPLOAD_AVAILABLE = 7;
|
||||
public static final int OPTION_UNVERIFIED = 8;
|
||||
public static final int OPTION_FIXED_USERNAME = 9;
|
||||
private static final String KEY_PGP_SIGNATURE = "pgp_signature";
|
||||
private static final String KEY_PGP_ID = "pgp_id";
|
||||
public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
|
||||
protected final JSONObject keys;
|
||||
private final Roster roster = new Roster(this);
|
||||
private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
|
||||
public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>();
|
||||
public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>();
|
||||
public final Set<Conversation> pendingConferenceJoins = new HashSet<>();
|
||||
public final Set<Conversation> pendingConferenceLeaves = new HashSet<>();
|
||||
public final Set<Conversation> inProgressConferenceJoins = new HashSet<>();
|
||||
public final Set<Conversation> inProgressConferencePings = new HashSet<>();
|
||||
protected Jid jid;
|
||||
protected String password;
|
||||
protected int options = 0;
|
||||
|
@ -91,7 +96,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
private XmppConnection xmppConnection = null;
|
||||
private long mEndGracePeriod = 0L;
|
||||
private String otrFingerprint;
|
||||
private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
|
||||
private final Map<Jid, Bookmark> bookmarks = new HashMap<>();
|
||||
private Presence.Status presenceStatus = Presence.Status.ONLINE;
|
||||
private String presenceStatusMessage = null;
|
||||
|
||||
|
@ -501,36 +506,45 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
return this.roster;
|
||||
}
|
||||
|
||||
public List<Bookmark> getBookmarks() {
|
||||
return this.bookmarks;
|
||||
public Collection<Bookmark> getBookmarks() {
|
||||
return this.bookmarks.values();
|
||||
}
|
||||
|
||||
public void setBookmarks(final CopyOnWriteArrayList<Bookmark> bookmarks) {
|
||||
this.bookmarks = bookmarks;
|
||||
public void setBookmarks(Map<Jid, Bookmark> bookmarks) {
|
||||
synchronized (this.bookmarks) {
|
||||
this.bookmarks.clear();
|
||||
this.bookmarks.putAll(bookmarks);
|
||||
}
|
||||
}
|
||||
|
||||
public void putBookmark(Bookmark bookmark) {
|
||||
synchronized (this.bookmarks) {
|
||||
this.bookmarks.put(bookmark.getJid(), bookmark);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeBookmark(Bookmark bookmark) {
|
||||
synchronized (this.bookmarks) {
|
||||
this.bookmarks.remove(bookmark.getJid());
|
||||
}
|
||||
}
|
||||
|
||||
public void removeBookmark(Jid jid) {
|
||||
synchronized (this.bookmarks) {
|
||||
this.bookmarks.remove(jid);
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Jid> getBookmarkedJids() {
|
||||
final Set<Jid> jids = new HashSet<>();
|
||||
for (final Bookmark bookmark : this.bookmarks) {
|
||||
final Jid jid = bookmark.getJid();
|
||||
if (jid != null) {
|
||||
jids.add(jid.asBareJid());
|
||||
}
|
||||
synchronized (this.bookmarks) {
|
||||
return new HashSet<>(this.bookmarks.keySet());
|
||||
}
|
||||
return jids;
|
||||
}
|
||||
|
||||
public boolean hasBookmarkFor(final Jid conferenceJid) {
|
||||
return getBookmark(conferenceJid) != null;
|
||||
}
|
||||
|
||||
Bookmark getBookmark(final Jid jid) {
|
||||
for (final Bookmark bookmark : this.bookmarks) {
|
||||
if (bookmark.getJid() != null && jid.asBareJid().equals(bookmark.getJid().asBareJid())) {
|
||||
return bookmark;
|
||||
}
|
||||
public Bookmark getBookmark(final Jid jid) {
|
||||
synchronized (this.bookmarks) {
|
||||
return this.bookmarks.get(jid.asBareJid());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean setAvatar(final String filename) {
|
||||
|
@ -638,6 +652,7 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
REGISTRATION_CONFLICT(true, false),
|
||||
REGISTRATION_NOT_SUPPORTED(true, false),
|
||||
REGISTRATION_PLEASE_WAIT(true, false),
|
||||
REGISTRATION_INVALID_TOKEN(true, false),
|
||||
REGISTRATION_PASSWORD_TOO_WEAK(true, false),
|
||||
TLS_ERROR,
|
||||
INCOMPATIBLE_SERVER,
|
||||
|
@ -702,6 +717,8 @@ public class Account extends AbstractEntity implements AvatarService.Avatarable
|
|||
return R.string.account_status_regis_success;
|
||||
case REGISTRATION_NOT_SUPPORTED:
|
||||
return R.string.account_status_regis_not_sup;
|
||||
case REGISTRATION_INVALID_TOKEN:
|
||||
return R.string.account_status_regis_invalid_token;
|
||||
case TLS_ERROR:
|
||||
return R.string.account_status_tls_error;
|
||||
case INCOMPATIBLE_SERVER:
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
package de.pixart.messenger.entities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import de.pixart.messenger.utils.Namespace;
|
||||
import de.pixart.messenger.utils.StringUtils;
|
||||
import de.pixart.messenger.utils.UIHelper;
|
||||
import de.pixart.messenger.xml.Element;
|
||||
|
@ -33,20 +38,70 @@ public class Bookmark extends Element implements ListItem {
|
|||
this.account = account;
|
||||
}
|
||||
|
||||
public static Map<Jid, Bookmark> parseFromStorage(Element storage, Account account) {
|
||||
if (storage == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
final HashMap<Jid, Bookmark> bookmarks = new HashMap<>();
|
||||
for (final Element item : storage.getChildren()) {
|
||||
if (item.getName().equals("conference")) {
|
||||
final Bookmark bookmark = Bookmark.parse(item, account);
|
||||
if (bookmark != null) {
|
||||
final Bookmark old = bookmarks.put(bookmark.jid, bookmark);
|
||||
if (old != null && old.getBookmarkName() != null && bookmark.getBookmarkName() == null) {
|
||||
bookmark.setBookmarkName(old.getBookmarkName());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
public static Map<Jid, Bookmark> parseFromPubsub(Element pubsub, Account account) {
|
||||
if (pubsub == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
final Element items = pubsub.findChild("items");
|
||||
if (items != null && Namespace.BOOKMARKS2.equals(items.getAttribute("node"))) {
|
||||
final Map<Jid, Bookmark> bookmarks = new HashMap<>();
|
||||
for (Element item : items.getChildren()) {
|
||||
if (item.getName().equals("item")) {
|
||||
final Bookmark bookmark = Bookmark.parseFromItem(item, account);
|
||||
if (bookmark != null) {
|
||||
bookmarks.put(bookmark.jid, bookmark);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bookmarks;
|
||||
}
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
public static Bookmark parse(Element element, Account account) {
|
||||
Bookmark bookmark = new Bookmark(account);
|
||||
bookmark.setAttributes(element.getAttributes());
|
||||
bookmark.setChildren(element.getChildren());
|
||||
bookmark.jid = InvalidJid.getNullForInvalid(bookmark.getAttributeAsJid("jid"));
|
||||
if (bookmark.jid == null) {
|
||||
return null;
|
||||
}
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
public static boolean printableValue(@Nullable String value, boolean permitNone) {
|
||||
return value != null && !value.trim().isEmpty() && (permitNone || !"None".equals(value));
|
||||
}
|
||||
|
||||
public static boolean printableValue(@Nullable String value) {
|
||||
return printableValue(value, true);
|
||||
public static Bookmark parseFromItem(Element item, Account account) {
|
||||
final Element conference = item.findChild("conference", Namespace.BOOKMARKS2);
|
||||
if (conference == null) {
|
||||
return null;
|
||||
}
|
||||
final Bookmark bookmark = new Bookmark(account);
|
||||
bookmark.jid = InvalidJid.getNullForInvalid(item.getAttributeAsJid("id"));
|
||||
if (bookmark.jid == null) {
|
||||
return null;
|
||||
}
|
||||
bookmark.setBookmarkName(conference.getAttribute("name"));
|
||||
bookmark.setAutojoin(conference.getAttributeAsBoolean("autojoin"));
|
||||
bookmark.setNick(conference.findChildContent("nick"));
|
||||
return bookmark;
|
||||
}
|
||||
|
||||
public void setAutojoin(boolean autojoin) {
|
||||
|
@ -82,6 +137,14 @@ public class Bookmark extends Element implements ListItem {
|
|||
return 0;
|
||||
}
|
||||
|
||||
public static boolean printableValue(@Nullable String value, boolean permitNone) {
|
||||
return value != null && !value.trim().isEmpty() && (permitNone || !"None".equals(value));
|
||||
}
|
||||
|
||||
public static boolean printableValue(@Nullable String value) {
|
||||
return printableValue(value, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Jid getJid() {
|
||||
return this.jid;
|
||||
|
@ -191,4 +254,4 @@ public class Bookmark extends Element implements ListItem {
|
|||
public int getAvatarBackgroundColor() {
|
||||
return UIHelper.getColorForName(jid != null ? jid.asBareJid().toString() : getDisplayName());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,9 +4,10 @@ import android.content.ContentValues;
|
|||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -20,6 +21,7 @@ import java.util.Locale;
|
|||
import de.pixart.messenger.Config;
|
||||
import de.pixart.messenger.R;
|
||||
import de.pixart.messenger.android.AbstractPhoneContact;
|
||||
import de.pixart.messenger.services.QuickConversationsService;
|
||||
import de.pixart.messenger.utils.JidHelper;
|
||||
import de.pixart.messenger.utils.UIHelper;
|
||||
import de.pixart.messenger.xml.Element;
|
||||
|
@ -101,7 +103,7 @@ public class Contact implements ListItem, Blockable {
|
|||
final Jid jid;
|
||||
try {
|
||||
jid = Jid.of(cursor.getString(cursor.getColumnIndex(JID)));
|
||||
} catch (final IllegalArgumentException e) {
|
||||
} catch (final IllegalArgumentException e) {
|
||||
// TODO: Borked DB... handle this somehow?
|
||||
return null;
|
||||
}
|
||||
|
@ -384,7 +386,9 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
|
||||
public boolean showInContactList() {
|
||||
return showInRoster() || getOption(Options.SYNCED_VIA_OTHER);
|
||||
return showInRoster()
|
||||
|| getOption(Options.SYNCED_VIA_OTHER)
|
||||
|| (QuickConversationsService.isQuicksy() && systemAccount != null);
|
||||
}
|
||||
|
||||
public void parseSubscriptionFromElement(Element item) {
|
||||
|
@ -587,7 +591,8 @@ public class Contact implements ListItem, Blockable {
|
|||
changed |= setPhotoUri(phoneContact.getPhotoUri());
|
||||
return changed;
|
||||
}
|
||||
public synchronized boolean unsetPhoneContact(Class<?extends AbstractPhoneContact> clazz) {
|
||||
|
||||
public synchronized boolean unsetPhoneContact(Class<? extends AbstractPhoneContact> clazz) {
|
||||
resetOption(getOption(clazz));
|
||||
boolean changed = false;
|
||||
if (!getOption(Options.SYNCED_VIA_ADDRESSBOOK) && !getOption(Options.SYNCED_VIA_OTHER)) {
|
||||
|
@ -597,6 +602,7 @@ public class Contact implements ListItem, Blockable {
|
|||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
|
||||
return Options.SYNCED_VIA_OTHER;
|
||||
}
|
||||
|
|
|
@ -2,10 +2,11 @@ package de.pixart.messenger.entities;
|
|||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.java.otr4j.OtrException;
|
||||
import net.java.otr4j.crypto.OtrCryptoException;
|
||||
import net.java.otr4j.session.SessionID;
|
||||
|
@ -57,6 +58,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
|
||||
public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
|
||||
public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
|
||||
public static final String ATTRIBUTE_PUSH_NODE = "push_node";
|
||||
public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
|
||||
static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
|
||||
private static final String ATTRIBUTE_NEXT_MESSAGE = "next_message";
|
||||
|
@ -87,8 +89,8 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
private transient MucOptions mucOptions = null;
|
||||
private byte[] symmetricKey;
|
||||
private boolean messagesLeftOnServer = true;
|
||||
private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
|
||||
private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
|
||||
private ChatState mOutgoingChatState = Config.DEFAULT_CHAT_STATE;
|
||||
private ChatState mIncomingChatState = Config.DEFAULT_CHAT_STATE;
|
||||
private String mLastReceivedOtrMessageId = null;
|
||||
private String mFirstMamReference = null;
|
||||
|
||||
|
@ -139,7 +141,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
|
||||
public void deleteMessage(Message message) {
|
||||
this.messages.remove(message);
|
||||
synchronized (this.messages) {
|
||||
this.messages.remove(message);
|
||||
}
|
||||
}
|
||||
|
||||
public Message getFirstUnreadMessage() {
|
||||
|
@ -338,7 +342,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
final ArrayList<Message> results = new ArrayList<>();
|
||||
synchronized (this.messages) {
|
||||
for (Message message : this.messages) {
|
||||
if (message.getType() != Message.TYPE_IMAGE && message.getStatus() == Message.STATUS_UNSEND) {
|
||||
if ((message.getType() == Message.TYPE_TEXT || message.hasFileOnRemoteHost()) && message.getStatus() == Message.STATUS_UNSEND) {
|
||||
results.add(message);
|
||||
}
|
||||
}
|
||||
|
@ -364,11 +368,18 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
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)
|
||||
final Message message = messages.get(i);
|
||||
final Jid mcp = message.getCounterpart();
|
||||
if (mcp == null) {
|
||||
continue;
|
||||
}
|
||||
final boolean counterpartMatch = mode == MODE_SINGLE ?
|
||||
counterpart.asBareJid().equals(mcp.asBareJid()) :
|
||||
counterpart.equals(mcp);
|
||||
if (counterpartMatch && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
|
||||
&& (carbon == message.isCarbon() || received)) {
|
||||
if (id.equals(message.getRemoteMsgId()) && !message.isFileOrImage() && !message.treatAsDownloadable()) {
|
||||
final boolean idMatch = id.equals(message.getRemoteMsgId()) || message.remoteMsgIdMatchInEdit(id);
|
||||
if (idMatch && !message.isFileOrImage() && !message.treatAsDownloadable()) {
|
||||
return message;
|
||||
} else {
|
||||
return null;
|
||||
|
@ -736,7 +747,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
}
|
||||
DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
|
||||
this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
|
||||
} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
|
||||
} catch (final OtrCryptoException ignored) {
|
||||
return null;
|
||||
} catch (final UnsupportedOperationException ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -798,6 +811,9 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
|
|||
if (OmemoSetting.isAlways()) {
|
||||
return suitableForOmemoByDefault(this) ? Message.ENCRYPTION_AXOLOTL : Message.ENCRYPTION_NONE;
|
||||
}
|
||||
if (OmemoSetting.isNever()) {
|
||||
return Message.ENCRYPTION_NONE;
|
||||
}
|
||||
final int defaultEncryption;
|
||||
if (suitableForOmemoByDefault(this)) {
|
||||
defaultEncryption = OmemoSetting.getEncryption();
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package de.pixart.messenger.entities;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import de.pixart.messenger.Config;
|
||||
import de.pixart.messenger.utils.MimeUtils;
|
||||
|
||||
public class DownloadableFile extends File {
|
||||
|
@ -67,6 +70,7 @@ public class DownloadableFile extends File {
|
|||
this.iv = new byte[]{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf};
|
||||
System.arraycopy(keyIvCombo, 0, aeskey, 0, 32);
|
||||
}
|
||||
Log.d(Config.LOGTAG, "using " + this.iv.length + "-byte IV for file transmission");
|
||||
}
|
||||
|
||||
public void setKey(byte[] key) {
|
||||
|
|
97
src/main/java/de/pixart/messenger/entities/Edit.java
Normal file
|
@ -0,0 +1,97 @@
|
|||
package de.pixart.messenger.entities;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Edit {
|
||||
|
||||
private final String editedId;
|
||||
private final String serverMsgId;
|
||||
|
||||
Edit(String editedId, String serverMsgId) {
|
||||
this.editedId = editedId;
|
||||
this.serverMsgId = serverMsgId;
|
||||
}
|
||||
|
||||
static String toJson(List<Edit> edits) throws JSONException {
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
for (Edit edit : edits) {
|
||||
jsonArray.put(edit.toJson());
|
||||
}
|
||||
return jsonArray.toString();
|
||||
}
|
||||
|
||||
static boolean wasPreviouslyEditedRemoteMsgId(List<Edit> edits, String remoteMsgId) {
|
||||
for (Edit edit : edits) {
|
||||
if (edit.editedId != null && edit.editedId.equals(remoteMsgId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean wasPreviouslyEditedServerMsgId(List<Edit> edits, String serverMsgId) {
|
||||
for (Edit edit : edits) {
|
||||
if (edit.serverMsgId != null && edit.serverMsgId.equals(serverMsgId)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Edit fromJson(JSONObject jsonObject) throws JSONException {
|
||||
String edited = jsonObject.has("edited_id") ? jsonObject.getString("edited_id") : null;
|
||||
String serverMsgId = jsonObject.has("server_msg_id") ? jsonObject.getString("server_msg_id") : null;
|
||||
return new Edit(edited, serverMsgId);
|
||||
}
|
||||
|
||||
static List<Edit> fromJson(String input) {
|
||||
final ArrayList<Edit> list = new ArrayList<>();
|
||||
if (input == null) {
|
||||
return list;
|
||||
}
|
||||
try {
|
||||
final JSONArray jsonArray = new JSONArray(input);
|
||||
for (int i = 0; i < jsonArray.length(); ++i) {
|
||||
list.add(fromJson(jsonArray.getJSONObject(i)));
|
||||
}
|
||||
return list;
|
||||
} catch (JSONException e) {
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject toJson() throws JSONException {
|
||||
JSONObject jsonObject = new JSONObject();
|
||||
jsonObject.put("edited_id", editedId);
|
||||
jsonObject.put("server_msg_id", serverMsgId);
|
||||
return jsonObject;
|
||||
}
|
||||
|
||||
String getEditedId() {
|
||||
return editedId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Edit edit = (Edit) o;
|
||||
|
||||
if (editedId != null ? !editedId.equals(edit.editedId) : edit.editedId != null)
|
||||
return false;
|
||||
return serverMsgId != null ? serverMsgId.equals(edit.serverMsgId) : edit.serverMsgId == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = editedId != null ? editedId.hashCode() : 0;
|
||||
result = 31 * result + (serverMsgId != null ? serverMsgId.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -41,8 +41,8 @@ public class IndividualMessage extends Message {
|
|||
super(conversation);
|
||||
}
|
||||
|
||||
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, boolean deleted, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean file_deleted) {
|
||||
super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, deleted, edited, oob, errorMessage, readByMarkers, markable, file_deleted);
|
||||
private IndividualMessage(Conversational conversation, String uuid, String conversationUUid, Jid counterpart, Jid trueCounterpart, String body, long timeSent, int encryption, int status, int type, boolean carbon, String remoteMsgId, String relativeFilePath, String serverMsgId, String fingerprint, boolean read, boolean deleted, String edited, boolean oob, String errorMessage, Set<ReadByMarker> readByMarkers, boolean markable, boolean file_deleted, String bodyLanguage) {
|
||||
super(conversation, uuid, conversationUUid, counterpart, trueCounterpart, body, timeSent, encryption, status, type, carbon, remoteMsgId, relativeFilePath, serverMsgId, fingerprint, read, deleted, edited, oob, errorMessage, readByMarkers, markable, file_deleted, bodyLanguage);
|
||||
}
|
||||
|
||||
public static Message createDateSeparator(Message message) {
|
||||
|
@ -100,7 +100,9 @@ public class IndividualMessage extends Message {
|
|||
cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
|
||||
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
|
||||
cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
|
||||
cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0);
|
||||
cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0,
|
||||
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -5,22 +5,22 @@ import android.database.Cursor;
|
|||
import android.graphics.Color;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import de.pixart.messenger.Config;
|
||||
import de.pixart.messenger.crypto.axolotl.FingerprintStatus;
|
||||
import de.pixart.messenger.services.AvatarService;
|
||||
import de.pixart.messenger.ui.util.MyLinkify;
|
||||
import de.pixart.messenger.utils.CryptoHelper;
|
||||
import de.pixart.messenger.utils.Emoticons;
|
||||
import de.pixart.messenger.utils.GeoHelper;
|
||||
|
@ -64,6 +64,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
public static final String COUNTERPART = "counterpart";
|
||||
public static final String TRUE_COUNTERPART = "trueCounterpart";
|
||||
public static final String BODY = "body";
|
||||
public static final String BODY_LANGUAGE = "bodyLanguage";
|
||||
public static final String TIME_SENT = "timeSent";
|
||||
public static final String ENCRYPTION = "encryption";
|
||||
public static final String STATUS = "status";
|
||||
|
@ -83,6 +84,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
public static final String FILE_DELETED = "file_deleted";
|
||||
public static final String ME_COMMAND = "/me";
|
||||
public static final String ERROR_MESSAGE_CANCELLED = "de.pixart.messenger.cancelled";
|
||||
public static final String DELETED_MESSAGE_BODY = "de.pixart.messenger.message_deleted";
|
||||
|
||||
public boolean markable = false;
|
||||
protected String conversationUuid;
|
||||
|
@ -97,11 +99,12 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
protected boolean file_deleted = false;
|
||||
protected boolean carbon = false;
|
||||
protected boolean oob = false;
|
||||
protected String edited = null;
|
||||
protected List<Edit> edits = new ArrayList<>();
|
||||
protected String relativeFilePath;
|
||||
protected boolean read = true;
|
||||
protected boolean deleted = false;
|
||||
protected String remoteMsgId = null;
|
||||
private String bodyLanguage = null;
|
||||
protected String serverMsgId = null;
|
||||
private final Conversational conversation;
|
||||
protected Transferable transferable = null;
|
||||
|
@ -150,7 +153,8 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
null,
|
||||
null,
|
||||
false,
|
||||
false);
|
||||
false,
|
||||
null);
|
||||
}
|
||||
|
||||
protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
|
||||
|
@ -159,7 +163,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
final String remoteMsgId, final String relativeFilePath,
|
||||
final String serverMsgId, final String fingerprint, final boolean read, final boolean deleted,
|
||||
final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
|
||||
final boolean markable, final boolean file_deleted) {
|
||||
final boolean markable, final boolean file_deleted, final String bodyLanguage) {
|
||||
this.conversation = conversation;
|
||||
this.uuid = uuid;
|
||||
this.conversationUuid = conversationUUid;
|
||||
|
@ -177,12 +181,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
this.axolotlFingerprint = fingerprint;
|
||||
this.read = read;
|
||||
this.deleted = deleted;
|
||||
this.edited = edited;
|
||||
this.edits = Edit.fromJson(edited);
|
||||
this.oob = oob;
|
||||
this.errorMessage = errorMessage;
|
||||
this.readByMarkers = readByMarkers == null ? new HashSet<ReadByMarker>() : readByMarkers;
|
||||
this.readByMarkers = readByMarkers == null ? new HashSet<>() : readByMarkers;
|
||||
this.markable = markable;
|
||||
this.file_deleted = file_deleted;
|
||||
this.bodyLanguage = bodyLanguage;
|
||||
}
|
||||
|
||||
public static Message fromCursor(Cursor cursor, Conversation conversation) {
|
||||
|
@ -208,7 +213,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
|
||||
ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
|
||||
cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
|
||||
cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0);
|
||||
cursor.getInt(cursor.getColumnIndex(FILE_DELETED)) > 0,
|
||||
cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
|
||||
);
|
||||
}
|
||||
|
||||
private static Jid fromString(String value) {
|
||||
|
@ -264,12 +271,17 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
values.put(FINGERPRINT, axolotlFingerprint);
|
||||
values.put(READ, read ? 1 : 0);
|
||||
values.put(DELETED, deleted ? 1 : 0);
|
||||
values.put(EDITED, edited);
|
||||
try {
|
||||
values.put(EDITED, Edit.toJson(edits));
|
||||
} catch (JSONException e) {
|
||||
Log.e(Config.LOGTAG, "error persisting json for edits", e);
|
||||
}
|
||||
values.put(OOB, oob ? 1 : 0);
|
||||
values.put(ERROR_MESSAGE, errorMessage);
|
||||
values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
|
||||
values.put(MARKABLE, markable ? 1 : 0);
|
||||
values.put(FILE_DELETED, file_deleted ? 1 : 0);
|
||||
values.put(BODY_LANGUAGE, bodyLanguage);
|
||||
return values;
|
||||
}
|
||||
|
||||
|
@ -392,6 +404,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
return this.deleted;
|
||||
}
|
||||
|
||||
public void setMessageDeleted(boolean deleted) {
|
||||
this.deleted = deleted;
|
||||
}
|
||||
|
||||
public boolean isFileDeleted() {
|
||||
return this.file_deleted;
|
||||
}
|
||||
|
@ -436,12 +452,32 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
this.carbon = carbon;
|
||||
}
|
||||
|
||||
public void setEdited(String edited) {
|
||||
this.edited = edited;
|
||||
public void putEdited(String edited, String serverMsgId) {
|
||||
final Edit edit = new Edit(edited, serverMsgId);
|
||||
if (this.edits.size() < 128 && !this.edits.contains(edit)) {
|
||||
this.edits.add(edit);
|
||||
}
|
||||
}
|
||||
|
||||
boolean remoteMsgIdMatchInEdit(String id) {
|
||||
for (Edit edit : this.edits) {
|
||||
if (id.equals(edit.getEditedId())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getBodyLanguage() {
|
||||
return this.bodyLanguage;
|
||||
}
|
||||
|
||||
public void setBodyLanguage(String language) {
|
||||
this.bodyLanguage = language;
|
||||
}
|
||||
|
||||
public boolean edited() {
|
||||
return this.edited != null;
|
||||
return this.edits.size() > 0;
|
||||
}
|
||||
|
||||
public void setTrueCounterpart(Jid trueCounterpart) {
|
||||
|
@ -495,7 +531,9 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
|
||||
boolean similar(Message message) {
|
||||
if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
|
||||
return this.serverMsgId.equals(message.getServerMsgId());
|
||||
return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
|
||||
} else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
|
||||
return true;
|
||||
} else if (this.body == null || this.counterpart == null) {
|
||||
return false;
|
||||
} else {
|
||||
|
@ -510,7 +548,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
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())) {
|
||||
if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
|
||||
return true;
|
||||
}
|
||||
return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
|
||||
|
@ -592,12 +630,15 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
this.getCounterpart() != null &&
|
||||
this.getCounterpart().equals(message.getCounterpart()) &&
|
||||
this.edited() == message.edited() &&
|
||||
!this.isMessageDeleted() == !message.isMessageDeleted() &&
|
||||
(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
|
||||
this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
|
||||
!message.isGeoUri() &&
|
||||
!this.isGeoUri() &&
|
||||
!message.isWebUri() &&
|
||||
!this.isWebUri() &&
|
||||
!message.isOOb() &&
|
||||
!this.isOOb() &&
|
||||
!message.treatAsDownloadable() &&
|
||||
!this.treatAsDownloadable() &&
|
||||
!message.getBody().startsWith(ME_COMMAND) &&
|
||||
|
@ -640,6 +681,10 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isOOb() {
|
||||
return oob;
|
||||
}
|
||||
|
||||
public static class MergeSeparator {
|
||||
}
|
||||
|
||||
|
@ -723,7 +768,19 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
}
|
||||
|
||||
public String getEditedId() {
|
||||
return edited;
|
||||
if (edits.size() > 0) {
|
||||
return edits.get(edits.size() - 1).getEditedId();
|
||||
} else {
|
||||
throw new IllegalStateException("Attempting to store unedited message");
|
||||
}
|
||||
}
|
||||
|
||||
public String getEditedIdWireFormat() {
|
||||
if (edits.size() > 0) {
|
||||
return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
|
||||
} else {
|
||||
throw new IllegalStateException("Attempting to store unedited message");
|
||||
}
|
||||
}
|
||||
|
||||
public void setOob(boolean isOob) {
|
||||
|
@ -792,7 +849,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
if (this.transferable != null) {
|
||||
fileParams.size = this.transferable.getFileSize();
|
||||
}
|
||||
String parts[] = body == null ? new String[0] : body.split("\\|");
|
||||
final String[] parts = body == null ? new String[0] : body.split("\\|");
|
||||
switch (parts.length) {
|
||||
case 1:
|
||||
try {
|
||||
|
@ -818,6 +875,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
|
|||
case 6:
|
||||
fileParams.url = parseUrl(parts[0]);
|
||||
fileParams.size = parseLong(parts[1]);
|
||||
fileParams.runtime = parseInt(parts[4]);
|
||||
fileParams.subject = parseString(parts[5]);
|
||||
break;
|
||||
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package de.pixart.messenger.entities;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
|
@ -95,7 +96,7 @@ public class MucOptions {
|
|||
public void resetChatState() {
|
||||
synchronized (users) {
|
||||
for (User user : users) {
|
||||
user.chatState = Config.DEFAULT_CHATSTATE;
|
||||
user.chatState = Config.DEFAULT_CHAT_STATE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -335,7 +336,7 @@ public class MucOptions {
|
|||
}
|
||||
|
||||
public boolean isContactInRoom(Contact contact) {
|
||||
return findUserByRealJid(contact.getJid().asBareJid()) != null;
|
||||
return contact != null && findUserByRealJid(contact.getJid().asBareJid()) != null;
|
||||
}
|
||||
|
||||
public boolean isUserInRoom(Jid jid) {
|
||||
|
@ -417,7 +418,7 @@ public class MucOptions {
|
|||
}
|
||||
}
|
||||
|
||||
private String getProposedNick() {
|
||||
public String getProposedNick() {
|
||||
final Bookmark bookmark = this.conversation.getBookmark();
|
||||
final String bookmarkedNick = normalize(account.getJid(), bookmark == null ? null : bookmark.getNick());
|
||||
if (bookmarkedNick != null) {
|
||||
|
@ -426,12 +427,16 @@ public class MucOptions {
|
|||
} else if (!conversation.getJid().isBareJid()) {
|
||||
return conversation.getJid().getResource();
|
||||
} else {
|
||||
final String displayName = normalize(account.getJid(), account.getDisplayName());
|
||||
if (displayName == null) {
|
||||
return JidHelper.localPartOrFallback(account.getJid());
|
||||
} else {
|
||||
return displayName;
|
||||
}
|
||||
return defaultNick(account);
|
||||
}
|
||||
}
|
||||
|
||||
public static String defaultNick(final Account account) {
|
||||
final String displayName = normalize(account.getJid(), account.getDisplayName());
|
||||
if (displayName == null) {
|
||||
return JidHelper.localPartOrFallback(account.getJid());
|
||||
} else {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -742,7 +747,7 @@ public class MucOptions {
|
|||
private long pgpKeyId = 0;
|
||||
private Avatar avatar;
|
||||
private MucOptions options;
|
||||
private ChatState chatState = Config.DEFAULT_CHATSTATE;
|
||||
private ChatState chatState = Config.DEFAULT_CHAT_STATE;
|
||||
|
||||
public User(MucOptions options, Jid fullJid) {
|
||||
this.options = options;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package de.pixart.messenger.entities;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
|
|
92
src/main/java/de/pixart/messenger/entities/RawBlockable.java
Normal file
|
@ -0,0 +1,92 @@
|
|||
package de.pixart.messenger.entities;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.pixart.messenger.utils.UIHelper;
|
||||
import rocks.xmpp.addr.Jid;
|
||||
|
||||
public class RawBlockable implements ListItem, Blockable {
|
||||
|
||||
private final Account account;
|
||||
private final Jid jid;
|
||||
|
||||
public RawBlockable(Account account, Jid jid) {
|
||||
this.account = account;
|
||||
this.jid = jid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlocked() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDomainBlocked() {
|
||||
throw new AssertionError("not implemented");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Jid getBlockedJid() {
|
||||
return this.jid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayName() {
|
||||
if (jid.isFullJid()) {
|
||||
return jid.getResource();
|
||||
} else {
|
||||
return jid.toEscapedString();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOffline() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Jid getJid() {
|
||||
return this.jid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Tag> getTags(Context context) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Context context, String needle) {
|
||||
if (TextUtils.isEmpty(needle)) {
|
||||
return true;
|
||||
}
|
||||
needle = needle.toLowerCase(Locale.US).trim();
|
||||
String[] parts = needle.split("\\s+");
|
||||
for (String part : parts) {
|
||||
if (!jid.toEscapedString().contains(part)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Account getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvatarBackgroundColor() {
|
||||
return UIHelper.getColorForName(jid.toEscapedString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(ListItem o) {
|
||||
return this.getDisplayName().compareToIgnoreCase(
|
||||
o.getDisplayName());
|
||||
}
|
||||
}
|
|
@ -111,12 +111,16 @@ public class ReadByMarker {
|
|||
ReadByMarker marker = new ReadByMarker();
|
||||
try {
|
||||
marker.fullJid = Jid.of(jsonObject.getString("fullJid"));
|
||||
} catch (JSONException | IllegalArgumentException e) {
|
||||
} catch (JSONException e) {
|
||||
marker.fullJid = null;
|
||||
} catch (IllegalArgumentException e) {
|
||||
marker.fullJid = null;
|
||||
}
|
||||
try {
|
||||
marker.realJid = Jid.of(jsonObject.getString("realJid"));
|
||||
} catch (JSONException | IllegalArgumentException e) {
|
||||
} catch (JSONException e) {
|
||||
marker.realJid = null;
|
||||
} catch (IllegalArgumentException e) {
|
||||
marker.realJid = null;
|
||||
}
|
||||
return marker;
|
||||
|
@ -125,7 +129,9 @@ public class ReadByMarker {
|
|||
public static Set<ReadByMarker> fromJsonString(String json) {
|
||||
try {
|
||||
return fromJson(new JSONArray(json));
|
||||
} catch (JSONException | NullPointerException e) {
|
||||
} catch (JSONException e) {
|
||||
return new HashSet<>();
|
||||
} catch (NullPointerException e) {
|
||||
return new HashSet<>();
|
||||
}
|
||||
}
|
||||
|
|
88
src/main/java/de/pixart/messenger/entities/Room.java
Normal file
|
@ -0,0 +1,88 @@
|
|||
package de.pixart.messenger.entities;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ComparisonChain;
|
||||
|
||||
import de.pixart.messenger.services.AvatarService;
|
||||
import de.pixart.messenger.utils.LanguageUtils;
|
||||
import de.pixart.messenger.utils.UIHelper;
|
||||
import rocks.xmpp.addr.Jid;
|
||||
|
||||
public class Room implements AvatarService.Avatarable, Comparable<Room> {
|
||||
|
||||
public String address;
|
||||
public String name;
|
||||
public String description;
|
||||
public String language;
|
||||
public int nusers;
|
||||
|
||||
public Room(String address, String name, String description, String language, int nusers) {
|
||||
this.address = address;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.language = language;
|
||||
this.nusers = nusers;
|
||||
}
|
||||
|
||||
public Room() {
|
||||
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Jid getRoom() {
|
||||
try {
|
||||
return Jid.of(address);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return LanguageUtils.convert(language);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvatarBackgroundColor() {
|
||||
Jid room = getRoom();
|
||||
return UIHelper.getColorForName(room != null ? room.asBareJid().toEscapedString() : name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Room room = (Room) o;
|
||||
return Objects.equal(address, room.address) &&
|
||||
Objects.equal(name, room.name) &&
|
||||
Objects.equal(description, room.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(address, name, description);
|
||||
}
|
||||
|
||||
|
||||
public boolean contains(String needle) {
|
||||
return Strings.nullToEmpty(name).contains(needle)
|
||||
|| Strings.nullToEmpty(description).contains(needle)
|
||||
|| Strings.nullToEmpty(address).contains(needle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Room o) {
|
||||
return ComparisonChain.start()
|
||||
.compare(o.nusers, nusers)
|
||||
.compare(Strings.nullToEmpty(name), Strings.nullToEmpty(o.name))
|
||||
.compare(Strings.nullToEmpty(address), Strings.nullToEmpty(o.address))
|
||||
.result();
|
||||
}
|
||||
}
|
|
@ -2,9 +2,10 @@ package de.pixart.messenger.entities;
|
|||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
@ -85,6 +86,18 @@ public class ServiceDiscoveryResult {
|
|||
}
|
||||
}
|
||||
|
||||
private ServiceDiscoveryResult() {
|
||||
this.hash = "sha-1";
|
||||
this.features = Collections.emptyList();
|
||||
this.identities = Collections.emptyList();
|
||||
this.ver = null;
|
||||
this.forms = Collections.emptyList();
|
||||
}
|
||||
|
||||
public static ServiceDiscoveryResult empty() {
|
||||
return new ServiceDiscoveryResult();
|
||||
}
|
||||
|
||||
public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
|
||||
this(
|
||||
cursor.getString(cursor.getColumnIndex(HASH)),
|
||||
|
@ -144,7 +157,7 @@ public class ServiceDiscoveryResult {
|
|||
}
|
||||
|
||||
public String getVer() {
|
||||
return new String(Base64.encode(this.ver, Base64.DEFAULT)).trim();
|
||||
return Base64.encodeToString(this.ver, Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
public List<Identity> getIdentities() {
|
||||
|
|
|
@ -19,6 +19,7 @@ public interface Transferable {
|
|||
"otr"
|
||||
);
|
||||
|
||||
int STATUS_WAITING = 0x199;
|
||||
int STATUS_UNKNOWN = 0x200;
|
||||
int STATUS_CHECKING = 0x201;
|
||||
int STATUS_FAILED = 0x202;
|
||||
|
@ -26,6 +27,7 @@ public interface Transferable {
|
|||
int STATUS_DOWNLOADING = 0x204;
|
||||
int STATUS_OFFER_CHECK_FILESIZE = 0x206;
|
||||
int STATUS_UPLOADING = 0x207;
|
||||
int STATUS_CANCELLED = 0x208;
|
||||
|
||||
|
||||
boolean start();
|
||||
|
|
|
@ -19,6 +19,7 @@ import de.pixart.messenger.entities.Account;
|
|||
import de.pixart.messenger.services.XmppConnectionService;
|
||||
import de.pixart.messenger.utils.Namespace;
|
||||
import de.pixart.messenger.utils.PhoneHelper;
|
||||
import de.pixart.messenger.xmpp.XmppConnection;
|
||||
import de.pixart.messenger.xmpp.jingle.stanzas.Content;
|
||||
|
||||
public abstract class AbstractGenerator {
|
||||
|
@ -29,6 +30,8 @@ public abstract class AbstractGenerator {
|
|||
Content.Version.FT_5.getNamespace(),
|
||||
Namespace.JINGLE_TRANSPORTS_S5B,
|
||||
Namespace.JINGLE_TRANSPORTS_IBB,
|
||||
Namespace.JINGLE_ENCRYPTED_TRANSPORT,
|
||||
Namespace.JINGLE_ENCRYPTED_TRANSPORT_OMEMO,
|
||||
"http://jabber.org/protocol/muc",
|
||||
"jabber:x:conference",
|
||||
Namespace.OOB,
|
||||
|
@ -36,7 +39,6 @@ public abstract class AbstractGenerator {
|
|||
"http://jabber.org/protocol/disco#info",
|
||||
"urn:xmpp:avatar:metadata+notify",
|
||||
Namespace.NICK + "+notify",
|
||||
Namespace.BOOKMARKS + "+notify",
|
||||
"urn:xmpp:ping",
|
||||
"jabber:iq:version",
|
||||
"http://jabber.org/protocol/chatstates"
|
||||
|
@ -100,8 +102,8 @@ public abstract class AbstractGenerator {
|
|||
for (String feature : getFeatures(account)) {
|
||||
s.append(feature).append('<');
|
||||
}
|
||||
byte[] sha1 = md.digest(s.toString().getBytes());
|
||||
return new String(Base64.encode(sha1, Base64.DEFAULT)).trim();
|
||||
final byte[] sha1 = md.digest(s.toString().getBytes());
|
||||
return Base64.encodeToString(sha1, Base64.NO_WRAP);
|
||||
}
|
||||
|
||||
public static String getTimestamp(long time) {
|
||||
|
@ -110,7 +112,8 @@ public abstract class AbstractGenerator {
|
|||
}
|
||||
|
||||
public List<String> getFeatures(Account account) {
|
||||
ArrayList<String> features = new ArrayList<>(Arrays.asList(FEATURES));
|
||||
final XmppConnection connection = account.getXmppConnection();
|
||||
final ArrayList<String> features = new ArrayList<>(Arrays.asList(FEATURES));
|
||||
if (mXmppConnectionService.confirmMessages()) {
|
||||
features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES));
|
||||
}
|
||||
|
@ -129,6 +132,11 @@ public abstract class AbstractGenerator {
|
|||
if (mXmppConnectionService.broadcastLastActivity()) {
|
||||
features.add(Namespace.IDLE);
|
||||
}
|
||||
if (connection != null && connection.getFeatures().bookmarks2()) {
|
||||
features.add(Namespace.BOOKMARKS2 + "+notify");
|
||||
} else {
|
||||
features.add(Namespace.BOOKMARKS + "+notify");
|
||||
}
|
||||
Collections.sort(features);
|
||||
return features;
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import de.pixart.messenger.Config;
|
|||
import de.pixart.messenger.R;
|
||||
import de.pixart.messenger.crypto.axolotl.AxolotlService;
|
||||
import de.pixart.messenger.entities.Account;
|
||||
import de.pixart.messenger.entities.Bookmark;
|
||||
import de.pixart.messenger.entities.Conversation;
|
||||
import de.pixart.messenger.entities.DownloadableFile;
|
||||
import de.pixart.messenger.services.MessageArchiveService;
|
||||
|
@ -57,12 +58,6 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket purgeOfflineMessages() {
|
||||
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
|
||||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket versionResponse(final IqPacket request) {
|
||||
final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT);
|
||||
Element query = packet.query("jabber:iq:version");
|
||||
|
@ -96,6 +91,12 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket purgeOfflineMessages() {
|
||||
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
packet.addChild("offline", Namespace.FLEXIBLE_OFFLINE_MESSAGE_RETRIEVAL).addChild("purge");
|
||||
return packet;
|
||||
}
|
||||
|
||||
protected IqPacket publish(final String node, final Element item, final Bundle options) {
|
||||
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
|
||||
|
@ -124,6 +125,10 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket retrieveBookmarks() {
|
||||
return retrieve(Namespace.BOOKMARKS2, null);
|
||||
}
|
||||
|
||||
public IqPacket publishNick(String nick) {
|
||||
final Element item = new Element("item");
|
||||
item.addChild("nick", Namespace.NICK).setContent(nick);
|
||||
|
@ -137,6 +142,16 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket deleteItem(final String node, final String id) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
final Element pubsub = packet.addChild("pubsub", Namespace.PUBSUB);
|
||||
final Element retract = pubsub.addChild("retract");
|
||||
retract.setAttribute("node", node);
|
||||
retract.setAttribute("notify", "true");
|
||||
retract.addChild("item").setAttribute("id", id);
|
||||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket publishAvatar(Avatar avatar, Bundle options) {
|
||||
final Element item = new Element("item");
|
||||
item.setAttribute("id", avatar.sha1sum);
|
||||
|
@ -145,9 +160,9 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return publish("urn:xmpp:avatar:data", item, options);
|
||||
}
|
||||
|
||||
public IqPacket publishElement(final String namespace, final Element element, final Bundle options) {
|
||||
public IqPacket publishElement(final String namespace, final Element element, String id, final Bundle options) {
|
||||
final Element item = new Element("item");
|
||||
item.setAttribute("id", "current");
|
||||
item.setAttribute("id", id);
|
||||
item.addChild(element);
|
||||
return publish(namespace, item, options);
|
||||
}
|
||||
|
@ -221,6 +236,21 @@ public class IqGenerator extends AbstractGenerator {
|
|||
return publish(AxolotlService.PEP_DEVICE_LIST, item, publishOptions);
|
||||
}
|
||||
|
||||
public Element publishBookmarkItem(final Bookmark bookmark) {
|
||||
final String name = bookmark.getBookmarkName();
|
||||
final String nick = bookmark.getNick();
|
||||
final boolean autojoin = bookmark.autojoin();
|
||||
final Element conference = new Element("conference", Namespace.BOOKMARKS2);
|
||||
if (name != null) {
|
||||
conference.setAttribute("name", name);
|
||||
}
|
||||
if (nick != null) {
|
||||
conference.addChild("nick").setContent(nick);
|
||||
}
|
||||
conference.setAttribute("autojoin", String.valueOf(autojoin));
|
||||
return conference;
|
||||
}
|
||||
|
||||
public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
|
||||
final Set<PreKeyRecord> preKeyRecords, final int deviceId, Bundle publishOptions) {
|
||||
final Element item = new Element("item");
|
||||
|
@ -229,17 +259,17 @@ public class IqGenerator extends AbstractGenerator {
|
|||
final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
|
||||
signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
|
||||
ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
|
||||
signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.DEFAULT));
|
||||
signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(), Base64.NO_WRAP));
|
||||
final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
|
||||
signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.DEFAULT));
|
||||
signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(), Base64.NO_WRAP));
|
||||
final Element identityKeyElement = bundle.addChild("identityKey");
|
||||
identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
|
||||
identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.NO_WRAP));
|
||||
|
||||
final Element prekeys = bundle.addChild("prekeys", AxolotlService.PEP_PREFIX);
|
||||
for (PreKeyRecord preKeyRecord : preKeyRecords) {
|
||||
final Element prekey = prekeys.addChild("preKeyPublic");
|
||||
prekey.setAttribute("preKeyId", preKeyRecord.getId());
|
||||
prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.DEFAULT));
|
||||
prekey.setContent(Base64.encodeToString(preKeyRecord.getKeyPair().getPublicKey().serialize(), Base64.NO_WRAP));
|
||||
}
|
||||
|
||||
return publish(AxolotlService.PEP_BUNDLES + ":" + deviceId, item, publishOptions);
|
||||
|
@ -253,13 +283,13 @@ public class IqGenerator extends AbstractGenerator {
|
|||
for (int i = 0; i < certificates.length; ++i) {
|
||||
try {
|
||||
Element certificate = chain.addChild("certificate");
|
||||
certificate.setContent(Base64.encodeToString(certificates[i].getEncoded(), Base64.DEFAULT));
|
||||
certificate.setContent(Base64.encodeToString(certificates[i].getEncoded(), Base64.NO_WRAP));
|
||||
certificate.setAttribute("index", i);
|
||||
} catch (CertificateEncodingException e) {
|
||||
Log.d(Config.LOGTAG, "could not encode certificate");
|
||||
}
|
||||
}
|
||||
verification.addChild("signature").setContent(Base64.encodeToString(signature, Base64.DEFAULT));
|
||||
verification.addChild("signature").setContent(Base64.encodeToString(signature, Base64.NO_WRAP));
|
||||
return publish(AxolotlService.PEP_VERIFICATION + ":" + deviceId, item);
|
||||
}
|
||||
|
||||
|
@ -304,7 +334,7 @@ public class IqGenerator extends AbstractGenerator {
|
|||
public IqPacket generateSetBlockRequest(final Jid jid, boolean reportSpam) {
|
||||
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
|
||||
final Element block = iq.addChild("block", Namespace.BLOCKING);
|
||||
final Element item = block.addChild("item").setAttribute("jid", jid.asBareJid().toString());
|
||||
final Element item = block.addChild("item").setAttribute("jid", jid.toEscapedString());
|
||||
if (reportSpam) {
|
||||
item.addChild("report", "urn:xmpp:reporting:0").addChild("spam");
|
||||
}
|
||||
|
@ -315,7 +345,7 @@ public class IqGenerator extends AbstractGenerator {
|
|||
public IqPacket generateSetUnblockRequest(final Jid jid) {
|
||||
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
|
||||
final Element block = iq.addChild("unblock", Namespace.BLOCKING);
|
||||
block.addChild("item").setAttribute("jid", jid.asBareJid().toString());
|
||||
block.addChild("item").setAttribute("jid", jid.toEscapedString());
|
||||
return iq;
|
||||
}
|
||||
|
||||
|
@ -433,14 +463,35 @@ public class IqGenerator extends AbstractGenerator {
|
|||
}
|
||||
|
||||
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
return pushTokenToAppServer(appServer, token, deviceId, null);
|
||||
}
|
||||
|
||||
public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId, Jid muc) {
|
||||
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
packet.setTo(appServer);
|
||||
Element command = packet.addChild("command", "http://jabber.org/protocol/commands");
|
||||
final Element command = packet.addChild("command", Namespace.COMMANDS);
|
||||
command.setAttribute("node", "register-push-fcm");
|
||||
command.setAttribute("action", "execute");
|
||||
Data data = new Data();
|
||||
final Data data = new Data();
|
||||
data.put("token", token);
|
||||
data.put("android-id", deviceId);
|
||||
if (muc != null) {
|
||||
data.put("muc", muc.toEscapedString());
|
||||
}
|
||||
data.submit();
|
||||
command.addChild(data);
|
||||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket unregisterChannelOnAppServer(Jid appServer, String deviceId, String channel) {
|
||||
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
packet.setTo(appServer);
|
||||
final Element command = packet.addChild("command", Namespace.COMMANDS);
|
||||
command.setAttribute("node", "unregister-push-fcm");
|
||||
command.setAttribute("action", "execute");
|
||||
final Data data = new Data();
|
||||
data.put("channel", channel);
|
||||
data.put("android-id", deviceId);
|
||||
data.submit();
|
||||
command.addChild(data);
|
||||
return packet;
|
||||
|
@ -464,7 +515,7 @@ public class IqGenerator extends AbstractGenerator {
|
|||
public IqPacket disablePush(final Jid jid, final String node) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
|
||||
Element disable = packet.addChild("disable", Namespace.PUSH);
|
||||
disable.setAttribute("jid", jid.toString());
|
||||
disable.setAttribute("jid", jid.toEscapedString());
|
||||
disable.setAttribute("node", node);
|
||||
return packet;
|
||||
}
|
||||
|
@ -522,4 +573,18 @@ public class IqGenerator extends AbstractGenerator {
|
|||
}
|
||||
return packet;
|
||||
}
|
||||
}
|
||||
|
||||
public IqPacket queryDiscoItems(Jid jid) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
|
||||
packet.setTo(jid);
|
||||
packet.addChild("query", Namespace.DISCO_ITEMS);
|
||||
return packet;
|
||||
}
|
||||
|
||||
public IqPacket queryDiscoInfo(Jid jid) {
|
||||
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
|
||||
packet.setTo(jid);
|
||||
packet.addChild("query", Namespace.DISCO_INFO);
|
||||
return packet;
|
||||
}
|
||||
}
|
|
@ -14,7 +14,6 @@ import de.pixart.messenger.Config;
|
|||
import de.pixart.messenger.crypto.axolotl.AxolotlService;
|
||||
import de.pixart.messenger.crypto.axolotl.XmppAxolotlMessage;
|
||||
import de.pixart.messenger.entities.Account;
|
||||
import de.pixart.messenger.entities.Contact;
|
||||
import de.pixart.messenger.entities.Conversation;
|
||||
import de.pixart.messenger.entities.Message;
|
||||
import de.pixart.messenger.http.P1S3UrlStreamHandler;
|
||||
|
@ -63,7 +62,7 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
packet.setId(message.getUuid());
|
||||
packet.addChild("origin-id", Namespace.STANZA_IDS).setAttribute("id", message.getUuid());
|
||||
if (message.edited()) {
|
||||
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedId());
|
||||
packet.addChild("replace", "urn:xmpp:message-correct:0").setAttribute("id", message.getEditedIdWireFormat());
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
@ -229,6 +228,10 @@ public class MessageGenerator extends AbstractGenerator {
|
|||
if (password != null) {
|
||||
x.setAttribute("password", password);
|
||||
}
|
||||
if (contact.isFullJid()) {
|
||||
packet.addChild("no-store", "urn:xmpp:hints");
|
||||
packet.addChild("no-copy", "urn:xmpp:hints");
|
||||
}
|
||||
return packet;
|
||||
}
|
||||
|
||||
|
|
|
@ -50,15 +50,20 @@ public class PresenceGenerator extends AbstractGenerator {
|
|||
return selfPresence(account, status, true);
|
||||
}
|
||||
|
||||
public PresencePacket selfPresence(Account account, Presence.Status status, boolean includePgpAnnouncement) {
|
||||
PresencePacket packet = new PresencePacket();
|
||||
if (status.toShowString() != null) {
|
||||
packet.addChild("show").setContent(status.toShowString());
|
||||
}
|
||||
packet.setFrom(account.getJid());
|
||||
final String sig = account.getPgpSignature();
|
||||
if (includePgpAnnouncement && sig != null && mXmppConnectionService.getPgpEngine() != null) {
|
||||
packet.addChild("x", "jabber:x:signed").setContent(sig);
|
||||
public PresencePacket selfPresence(final Account account, final Presence.Status status, final boolean personal) {
|
||||
final PresencePacket packet = new PresencePacket();
|
||||
if (personal) {
|
||||
final String sig = account.getPgpSignature();
|
||||
final String message = account.getPresenceStatusMessage();
|
||||
if (status.toShowString() != null) {
|
||||
packet.addChild("show").setContent(status.toShowString());
|
||||
}
|
||||
if (!TextUtils.isEmpty(message)) {
|
||||
packet.addChild(new Element("status").setContent(message));
|
||||
}
|
||||
if (sig != null && mXmppConnectionService.getPgpEngine() != null) {
|
||||
packet.addChild("x", "jabber:x:signed").setContent(sig);
|
||||
}
|
||||
}
|
||||
final String capHash = getCapHash(account);
|
||||
if (capHash != null) {
|
||||
|
|
|
@ -104,7 +104,8 @@ public class HttpConnectionManager extends AbstractConnectionManager {
|
|||
final SSLSocketFactory sf = new TLSSocketFactory(new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG());
|
||||
connection.setSSLSocketFactory(sf);
|
||||
connection.setHostnameVerifier(hostnameVerifier);
|
||||
} catch (final KeyManagementException | NoSuchAlgorithmException ignored) {
|
||||
} catch (final KeyManagementException ignored) {
|
||||
} catch (final NoSuchAlgorithmException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,14 @@
|
|||
package de.pixart.messenger.http;
|
||||
|
||||
import android.os.PowerManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.common.io.ByteStreams;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
@ -36,16 +40,15 @@ import rocks.xmpp.addr.Jid;
|
|||
|
||||
public class HttpDownloadConnection implements Transferable {
|
||||
|
||||
private final Message message;
|
||||
private final boolean mUseTor;
|
||||
private HttpConnectionManager mHttpConnectionManager;
|
||||
private XmppConnectionService mXmppConnectionService;
|
||||
|
||||
private URL mUrl;
|
||||
private final Message message;
|
||||
private DownloadableFile file;
|
||||
private int mStatus = Transferable.STATUS_UNKNOWN;
|
||||
private boolean acceptedAutomatically = false;
|
||||
private int mProgress = 0;
|
||||
private final boolean mUseTor;
|
||||
private boolean canceled = false;
|
||||
private Method method = Method.HTTP_UPLOAD;
|
||||
|
||||
|
@ -73,37 +76,51 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
|
||||
public void init(boolean interactive) {
|
||||
if (message.isFileDeleted()) {
|
||||
if (message.getType() == Message.TYPE_PRIVATE_FILE) {
|
||||
message.setType(Message.TYPE_PRIVATE);
|
||||
} else if (message.isFileOrImage()) {
|
||||
message.setType(Message.TYPE_TEXT);
|
||||
}
|
||||
message.setOob(true);
|
||||
message.setFileDeleted(false);
|
||||
mXmppConnectionService.updateMessage(message);
|
||||
}
|
||||
this.message.setTransferable(this);
|
||||
try {
|
||||
final Message.FileParams fileParams = message.getFileParams();
|
||||
if (message.hasFileOnRemoteHost()) {
|
||||
mUrl = CryptoHelper.toHttpsUrl(message.getFileParams().url);
|
||||
mUrl = CryptoHelper.toHttpsUrl(fileParams.url);
|
||||
} else if (message.isOOb() && fileParams.url != null && fileParams.size > 0) {
|
||||
mUrl = fileParams.url;
|
||||
} else {
|
||||
mUrl = CryptoHelper.toHttpsUrl(new URL(message.getBody().split("\n")[0]));
|
||||
}
|
||||
String[] parts = mUrl.getPath().toLowerCase().split("\\.");
|
||||
String lastPart = parts.length >= 1 ? parts[parts.length - 1] : null;
|
||||
String secondToLast = parts.length >= 2 ? parts[parts.length - 2] : null;
|
||||
if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
|
||||
final AbstractConnectionManager.Extension extension = AbstractConnectionManager.Extension.of(mUrl.getPath());
|
||||
if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
|
||||
this.message.setEncryption(Message.ENCRYPTION_PGP);
|
||||
} else if (message.getEncryption() != Message.ENCRYPTION_OTR
|
||||
&& message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
|
||||
this.message.setEncryption(Message.ENCRYPTION_NONE);
|
||||
}
|
||||
String extension;
|
||||
if (VALID_CRYPTO_EXTENSIONS.contains(lastPart)) {
|
||||
extension = secondToLast;
|
||||
final String ext;
|
||||
if (VALID_CRYPTO_EXTENSIONS.contains(extension.main)) {
|
||||
ext = extension.secondary;
|
||||
} else {
|
||||
extension = lastPart;
|
||||
ext = extension.main;
|
||||
}
|
||||
if (message.getStatus() == Message.STATUS_RECEIVED){
|
||||
message.setRelativeFilePath(fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4) + (extension != null ? ("." + extension) : ""));
|
||||
if (message.getStatus() == Message.STATUS_RECEIVED) {
|
||||
message.setRelativeFilePath(fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4) + (ext != null ? ("." + ext) : ""));
|
||||
} else {
|
||||
message.setRelativeFilePath("Sent/" + fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4) + (extension != null ? ("." + extension) : ""));
|
||||
message.setRelativeFilePath("Sent/" + fileDateFormat.format(new Date(message.getTimeSent())) + "_" + message.getUuid().substring(0, 4) + (ext != null ? ("." + ext) : ""));
|
||||
}
|
||||
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
|
||||
final String reference = mUrl.getRef();
|
||||
if (reference != null && AesGcmURLStreamHandler.IV_KEY.matcher(reference).matches()) {
|
||||
this.file = new DownloadableFile(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + message.getUuid());
|
||||
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
|
||||
Log.d(Config.LOGTAG, "create temporary OMEMO encrypted file: " + this.file.getAbsolutePath() + "(" + message.getMimeType() + ")");
|
||||
} else {
|
||||
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
|
||||
}
|
||||
|
||||
if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
|
||||
|
@ -143,6 +160,33 @@ public class HttpDownloadConnection implements Transferable {
|
|||
mHttpConnectionManager.updateConversationUi(true);
|
||||
}
|
||||
|
||||
private void decryptFile() throws IOException {
|
||||
final DownloadableFile outputFile = mXmppConnectionService.getFileBackend().getFile(message, true);
|
||||
|
||||
if (outputFile.getParentFile().mkdirs()) {
|
||||
Log.d(Config.LOGTAG, "created parent directories for " + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
if (!outputFile.createNewFile()) {
|
||||
Log.w(Config.LOGTAG, "unable to create output file " + outputFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
final InputStream is = new FileInputStream(this.file);
|
||||
|
||||
outputFile.setKey(this.file.getKey());
|
||||
outputFile.setIv(this.file.getIv());
|
||||
final OutputStream os = AbstractConnectionManager.createOutputStream(outputFile, false, true);
|
||||
|
||||
ByteStreams.copy(is, os);
|
||||
|
||||
FileBackend.close(is);
|
||||
FileBackend.close(os);
|
||||
|
||||
if (!file.delete()) {
|
||||
Log.w(Config.LOGTAG, "unable to delete temporary OMEMO encrypted file " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
private void finish() {
|
||||
message.setTransferable(null);
|
||||
mHttpConnectionManager.finishConnection(this);
|
||||
|
@ -152,6 +196,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
mHttpConnectionManager.updateConversationUi(true);
|
||||
final boolean notifyAfterScan = notify;
|
||||
final DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message, true);
|
||||
mXmppConnectionService.getFileBackend().updateMediaScanner(file, () -> {
|
||||
if (notifyAfterScan) {
|
||||
mXmppConnectionService.getNotificationService().push(message);
|
||||
|
@ -159,6 +204,12 @@ public class HttpDownloadConnection implements Transferable {
|
|||
});
|
||||
}
|
||||
|
||||
private void decryptIfNeeded() throws IOException {
|
||||
if (file.getKey() != null && file.getIv() != null) {
|
||||
decryptFile();
|
||||
}
|
||||
}
|
||||
|
||||
private void changeStatus(int status) {
|
||||
this.mStatus = status;
|
||||
mHttpConnectionManager.updateConversationUi(true);
|
||||
|
@ -212,6 +263,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
this.interactive = interactive;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mUrl.getProtocol().equalsIgnoreCase(P1S3UrlStreamHandler.PROTOCOL_NAME)) {
|
||||
|
@ -265,6 +317,10 @@ public class HttpDownloadConnection implements Transferable {
|
|||
retrieveFailed(e);
|
||||
return;
|
||||
}
|
||||
final Message.FileParams fileParams = message.getFileParams();
|
||||
FileBackend.updateFileParams(message, fileParams.url, size);
|
||||
message.setOob(true);
|
||||
mXmppConnectionService.databaseBackend.updateMessage(message, true);
|
||||
file.setExpectedSize(size);
|
||||
message.resetFileParams();
|
||||
if (mHttpConnectionManager.hasStoragePermission()
|
||||
|
@ -300,7 +356,6 @@ public class HttpDownloadConnection implements Transferable {
|
|||
connection.setUseCaches(false);
|
||||
Log.d(Config.LOGTAG, "url: " + connection.getURL().toString());
|
||||
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
|
||||
connection.setRequestProperty("Accept-Encoding", "identity");
|
||||
if (connection instanceof HttpsURLConnection) {
|
||||
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
|
||||
}
|
||||
|
@ -346,22 +401,26 @@ public class HttpDownloadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
changeStatus(STATUS_DOWNLOADING);
|
||||
download();
|
||||
updateImageBounds();
|
||||
finish();
|
||||
} catch (SSLHandshakeException e) {
|
||||
changeStatus(STATUS_OFFER);
|
||||
} catch (Exception e) {
|
||||
if (interactive) {
|
||||
showToastForException(e);
|
||||
} else {
|
||||
HttpDownloadConnection.this.acceptedAutomatically = false;
|
||||
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
|
||||
changeStatus(STATUS_WAITING);
|
||||
mXmppConnectionService.mDownloadExecutor.execute(() -> {
|
||||
try {
|
||||
changeStatus(STATUS_DOWNLOADING);
|
||||
download();
|
||||
decryptIfNeeded();
|
||||
updateImageBounds();
|
||||
finish();
|
||||
} catch (SSLHandshakeException e) {
|
||||
changeStatus(STATUS_OFFER);
|
||||
} catch (Exception e) {
|
||||
if (interactive) {
|
||||
showToastForException(e);
|
||||
} else {
|
||||
HttpDownloadConnection.this.acceptedAutomatically = false;
|
||||
HttpDownloadConnection.this.mXmppConnectionService.getNotificationService().push(message);
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void download() throws Exception {
|
||||
|
@ -380,17 +439,17 @@ public class HttpDownloadConnection implements Transferable {
|
|||
}
|
||||
connection.setUseCaches(false);
|
||||
connection.setRequestProperty("User-Agent", mXmppConnectionService.getIqGenerator().getUserAgent());
|
||||
connection.setRequestProperty("Accept-Encoding", "identity");
|
||||
final long expected = file.getExpectedSize();
|
||||
final boolean tryResume = file.exists() && file.getKey() == null && file.getSize() > 0 && file.getSize() < expected;
|
||||
final boolean tryResume = file.exists() && file.getSize() > 0 && file.getSize() < expected;
|
||||
long resumeSize = 0;
|
||||
|
||||
if (tryResume) {
|
||||
resumeSize = file.getSize();
|
||||
Log.d(Config.LOGTAG, "http download trying resume after" + resumeSize + " of " + expected);
|
||||
Log.d(Config.LOGTAG, "http download trying resume after " + resumeSize + " of " + expected);
|
||||
connection.setRequestProperty("Range", "bytes=" + resumeSize + "-");
|
||||
}
|
||||
connection.setConnectTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(Config.CONNECT_TIMEOUT * 1000);
|
||||
connection.setReadTimeout(Config.SOCKET_TIMEOUT * 1000);
|
||||
connection.connect();
|
||||
is = new BufferedInputStream(connection.getInputStream());
|
||||
final String contentRange = connection.getHeaderField("Content-Range");
|
||||
|
@ -400,7 +459,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
Log.d(Config.LOGTAG, "server resumed");
|
||||
transmitted = file.getSize();
|
||||
updateProgress(Math.round(((double) transmitted / expected) * 100));
|
||||
os = AbstractConnectionManager.createAppendedOutputStream(file);
|
||||
os = AbstractConnectionManager.createOutputStream(file, true, false);
|
||||
if (os == null) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
|
@ -408,7 +467,9 @@ public class HttpDownloadConnection implements Transferable {
|
|||
long reportedContentLengthOnGet;
|
||||
try {
|
||||
reportedContentLengthOnGet = Long.parseLong(connection.getHeaderField("Content-Length"));
|
||||
} catch (NumberFormatException | NullPointerException e) {
|
||||
} catch (NumberFormatException e) {
|
||||
reportedContentLengthOnGet = 0;
|
||||
} catch (NullPointerException e) {
|
||||
reportedContentLengthOnGet = 0;
|
||||
}
|
||||
if (expected != reportedContentLengthOnGet) {
|
||||
|
@ -418,7 +479,7 @@ public class HttpDownloadConnection implements Transferable {
|
|||
if (!file.exists() && !file.createNewFile()) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
os = AbstractConnectionManager.createOutputStream(file, true);
|
||||
os = AbstractConnectionManager.createOutputStream(file, false, false);
|
||||
}
|
||||
int count;
|
||||
byte[] buffer = new byte[4096];
|
||||
|
@ -439,8 +500,11 @@ public class HttpDownloadConnection implements Transferable {
|
|||
} catch (IOException e) {
|
||||
throw new FileWriterException();
|
||||
}
|
||||
} catch (CancellationException | IOException e) {
|
||||
Log.d(Config.LOGTAG, "http download failed " + e.getMessage());
|
||||
} catch (CancellationException e) {
|
||||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": http download canceled", e);
|
||||
throw e;
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG, message.getConversation().getAccount().getJid().asBareJid() + ": http download failed", e);
|
||||
throw e;
|
||||
} finally {
|
||||
FileBackend.close(os);
|
||||
|
|
|
@ -47,6 +47,7 @@ public class HttpUploadConnection implements Transferable {
|
|||
private String mime;
|
||||
private SlotRequester.Slot slot;
|
||||
private byte[] key = null;
|
||||
private int mStatus = Transferable.STATUS_UNKNOWN;
|
||||
|
||||
private long transmitted = 0;
|
||||
|
||||
|
@ -66,7 +67,7 @@ public class HttpUploadConnection implements Transferable {
|
|||
|
||||
@Override
|
||||
public int getStatus() {
|
||||
return STATUS_UPLOADING;
|
||||
return this.mStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -105,11 +106,12 @@ public class HttpUploadConnection implements Transferable {
|
|||
} else {
|
||||
this.mime = this.file.getMimeType();
|
||||
}
|
||||
final long originalFileSize = file.getSize();
|
||||
this.delayed = delay;
|
||||
if (Config.ENCRYPT_ON_HTTP_UPLOADED
|
||||
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|
||||
|| message.getEncryption() == Message.ENCRYPTION_OTR) {
|
||||
this.key = new byte[48];
|
||||
this.key = new byte[44];
|
||||
mXmppConnectionService.getRNG().nextBytes(this.key);
|
||||
this.file.setKeyAndIv(this.key);
|
||||
}
|
||||
|
@ -128,14 +130,18 @@ public class HttpUploadConnection implements Transferable {
|
|||
md5 = null;
|
||||
}
|
||||
|
||||
this.file.setExpectedSize(file.getSize() + (file.getKey() != null ? 16 : 0));
|
||||
this.file.setExpectedSize(originalFileSize + (file.getKey() != null ? 16 : 0));
|
||||
message.resetFileParams();
|
||||
this.mSlotRequester.request(method, account, file, mime, md5, new SlotRequester.OnSlotRequested() {
|
||||
@Override
|
||||
public void success(SlotRequester.Slot slot) {
|
||||
if (!cancelled) {
|
||||
HttpUploadConnection.this.slot = slot;
|
||||
new Thread(HttpUploadConnection.this::upload).start();
|
||||
changeStatus(STATUS_WAITING);
|
||||
mXmppConnectionService.mUploadExecutor.execute(() -> {
|
||||
changeStatus(STATUS_UPLOADING);
|
||||
HttpUploadConnection.this.slot = slot;
|
||||
HttpUploadConnection.this.upload();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,6 +246,11 @@ public class HttpUploadConnection implements Transferable {
|
|||
}
|
||||
}
|
||||
|
||||
private void changeStatus(int status) {
|
||||
this.mStatus = status;
|
||||
mHttpConnectionManager.updateConversationUi(true);
|
||||
}
|
||||
|
||||
public Message getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
|
419
src/main/java/de/pixart/messenger/http/NoSSLv3SocketFactory.java
Normal file
|
@ -0,0 +1,419 @@
|
|||
package de.pixart.messenger.http;
|
||||
|
||||
/*Copyright 2015 Bhavit Singh Sengar
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.*/
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
import java.net.SocketException;
|
||||
import java.nio.channels.SocketChannel;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import javax.net.ssl.HandshakeCompletedListener;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSession;
|
||||
import javax.net.ssl.SSLSocket;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
|
||||
public class NoSSLv3SocketFactory extends SSLSocketFactory {
|
||||
private final SSLSocketFactory delegate;
|
||||
|
||||
public NoSSLv3SocketFactory() {
|
||||
this.delegate = HttpsURLConnection.getDefaultSSLSocketFactory();
|
||||
}
|
||||
|
||||
public NoSSLv3SocketFactory(SSLSocketFactory delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getDefaultCipherSuites() {
|
||||
return delegate.getDefaultCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return delegate.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
private Socket makeSocketSafe(Socket socket) {
|
||||
if (socket instanceof SSLSocket) {
|
||||
socket = new NoSSLv3SSLSocket((SSLSocket) socket);
|
||||
}
|
||||
return socket;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
|
||||
return makeSocketSafe(delegate.createSocket(s, host, port, autoClose));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port) throws IOException {
|
||||
return makeSocketSafe(delegate.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
|
||||
return makeSocketSafe(delegate.createSocket(host, port, localHost, localPort));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress host, int port) throws IOException {
|
||||
return makeSocketSafe(delegate.createSocket(host, port));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
|
||||
return makeSocketSafe(delegate.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
|
||||
private class NoSSLv3SSLSocket extends DelegateSSLSocket {
|
||||
|
||||
private NoSSLv3SSLSocket(SSLSocket delegate) {
|
||||
super(delegate);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabledProtocols(String[] protocols) {
|
||||
if (protocols != null && protocols.length == 1 && "SSLv3".equals(protocols[0])) {
|
||||
|
||||
List<String> enabledProtocols = new ArrayList<String>(Arrays.asList(delegate.getEnabledProtocols()));
|
||||
if (enabledProtocols.size() > 1) {
|
||||
enabledProtocols.remove("SSLv3");
|
||||
System.out.println("Removed SSLv3 from enabled protocols");
|
||||
} else {
|
||||
System.out.println("SSL stuck with protocol available for " + String.valueOf(enabledProtocols));
|
||||
}
|
||||
protocols = enabledProtocols.toArray(new String[enabledProtocols.size()]);
|
||||
}
|
||||
|
||||
super.setEnabledProtocols(protocols);
|
||||
}
|
||||
}
|
||||
|
||||
public class DelegateSSLSocket extends SSLSocket {
|
||||
|
||||
protected final SSLSocket delegate;
|
||||
|
||||
DelegateSSLSocket(SSLSocket delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedCipherSuites() {
|
||||
return delegate.getSupportedCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getEnabledCipherSuites() {
|
||||
return delegate.getEnabledCipherSuites();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabledCipherSuites(String[] suites) {
|
||||
delegate.setEnabledCipherSuites(suites);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getSupportedProtocols() {
|
||||
return delegate.getSupportedProtocols();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getEnabledProtocols() {
|
||||
return delegate.getEnabledProtocols();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnabledProtocols(String[] protocols) {
|
||||
delegate.setEnabledProtocols(protocols);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SSLSession getSession() {
|
||||
return delegate.getSession();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHandshakeCompletedListener(HandshakeCompletedListener listener) {
|
||||
delegate.addHandshakeCompletedListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeHandshakeCompletedListener(HandshakeCompletedListener listener) {
|
||||
delegate.removeHandshakeCompletedListener(listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startHandshake() throws IOException {
|
||||
delegate.startHandshake();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseClientMode(boolean mode) {
|
||||
delegate.setUseClientMode(mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getUseClientMode() {
|
||||
return delegate.getUseClientMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNeedClientAuth(boolean need) {
|
||||
delegate.setNeedClientAuth(need);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWantClientAuth(boolean want) {
|
||||
delegate.setWantClientAuth(want);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getNeedClientAuth() {
|
||||
return delegate.getNeedClientAuth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getWantClientAuth() {
|
||||
return delegate.getWantClientAuth();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEnableSessionCreation(boolean flag) {
|
||||
delegate.setEnableSessionCreation(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getEnableSessionCreation() {
|
||||
return delegate.getEnableSessionCreation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(SocketAddress localAddr) throws IOException {
|
||||
delegate.bind(localAddr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void close() throws IOException {
|
||||
delegate.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(SocketAddress remoteAddr) throws IOException {
|
||||
delegate.connect(remoteAddr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(SocketAddress remoteAddr, int timeout) throws IOException {
|
||||
delegate.connect(remoteAddr, timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketChannel getChannel() {
|
||||
return delegate.getChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetAddress getInetAddress() {
|
||||
return delegate.getInetAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return delegate.getInputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getKeepAlive() throws SocketException {
|
||||
return delegate.getKeepAlive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetAddress getLocalAddress() {
|
||||
return delegate.getLocalAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLocalPort() {
|
||||
return delegate.getLocalPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getLocalSocketAddress() {
|
||||
return delegate.getLocalSocketAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getOOBInline() throws SocketException {
|
||||
return delegate.getOOBInline();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() throws IOException {
|
||||
return delegate.getOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
return delegate.getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getReceiveBufferSize() throws SocketException {
|
||||
return delegate.getReceiveBufferSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getRemoteSocketAddress() {
|
||||
return delegate.getRemoteSocketAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getReuseAddress() throws SocketException {
|
||||
return delegate.getReuseAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getSendBufferSize() throws SocketException {
|
||||
return delegate.getSendBufferSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSoLinger() throws SocketException {
|
||||
return delegate.getSoLinger();
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getSoTimeout() throws SocketException {
|
||||
return delegate.getSoTimeout();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getTcpNoDelay() throws SocketException {
|
||||
return delegate.getTcpNoDelay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTrafficClass() throws SocketException {
|
||||
return delegate.getTrafficClass();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBound() {
|
||||
return delegate.isBound();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isClosed() {
|
||||
return delegate.isClosed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return delegate.isConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInputShutdown() {
|
||||
return delegate.isInputShutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOutputShutdown() {
|
||||
return delegate.isOutputShutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendUrgentData(int value) throws IOException {
|
||||
delegate.sendUrgentData(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setKeepAlive(boolean keepAlive) throws SocketException {
|
||||
delegate.setKeepAlive(keepAlive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOOBInline(boolean oobinline) throws SocketException {
|
||||
delegate.setOOBInline(oobinline);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPerformancePreferences(int connectionTime, int latency, int bandwidth) {
|
||||
delegate.setPerformancePreferences(connectionTime, latency, bandwidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setReceiveBufferSize(int size) throws SocketException {
|
||||
delegate.setReceiveBufferSize(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReuseAddress(boolean reuse) throws SocketException {
|
||||
delegate.setReuseAddress(reuse);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setSendBufferSize(int size) throws SocketException {
|
||||
delegate.setSendBufferSize(size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSoLinger(boolean on, int timeout) throws SocketException {
|
||||
delegate.setSoLinger(on, timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void setSoTimeout(int timeout) throws SocketException {
|
||||
delegate.setSoTimeout(timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTcpNoDelay(boolean on) throws SocketException {
|
||||
delegate.setTcpNoDelay(on);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTrafficClass(int value) throws SocketException {
|
||||
delegate.setTrafficClass(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownInput() throws IOException {
|
||||
delegate.shutdownInput();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownOutput() throws IOException {
|
||||
delegate.shutdownOutput();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return delegate.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return delegate.equals(o);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,15 @@
|
|||
package de.pixart.messenger.http.services;
|
||||
|
||||
import com.google.common.base.Objects;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import de.pixart.messenger.services.AvatarService;
|
||||
import de.pixart.messenger.utils.LanguageUtils;
|
||||
import de.pixart.messenger.utils.UIHelper;
|
||||
import de.pixart.messenger.entities.Room;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Query;
|
||||
import rocks.xmpp.addr.Jid;
|
||||
|
||||
public interface MuclumbusService {
|
||||
|
||||
|
@ -31,57 +26,8 @@ public interface MuclumbusService {
|
|||
public List<Room> items;
|
||||
}
|
||||
|
||||
class Room implements AvatarService.Avatarable {
|
||||
|
||||
public String address;
|
||||
public String name;
|
||||
public String description;
|
||||
public String language;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public Jid getRoom() {
|
||||
try {
|
||||
return Jid.of(address);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public String getLanguage() {
|
||||
return LanguageUtils.convert(language);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAvatarBackgroundColor() {
|
||||
Jid room = getRoom();
|
||||
return UIHelper.getColorForName(room != null ? room.asBareJid().toEscapedString() : name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
Room room = (Room) o;
|
||||
return Objects.equal(address, room.address) &&
|
||||
Objects.equal(name, room.name) &&
|
||||
Objects.equal(description, room.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hashCode(address, name, description);
|
||||
}
|
||||
}
|
||||
|
||||
class SearchRequest {
|
||||
public Set<String> keywords;
|
||||
public final Set<String> keywords;
|
||||
|
||||
public SearchRequest(String keyword) {
|
||||
this.keywords = Collections.singleton(keyword);
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
package de.pixart.messenger.parser;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.ecc.Curve;
|
||||
import org.whispersystems.libsignal.ecc.ECPublicKey;
|
||||
|
@ -27,12 +29,14 @@ import de.pixart.messenger.crypto.axolotl.AxolotlService;
|
|||
import de.pixart.messenger.entities.Account;
|
||||
import de.pixart.messenger.entities.Contact;
|
||||
import de.pixart.messenger.entities.Conversation;
|
||||
import de.pixart.messenger.entities.Room;
|
||||
import de.pixart.messenger.services.XmppConnectionService;
|
||||
import de.pixart.messenger.utils.Namespace;
|
||||
import de.pixart.messenger.xml.Element;
|
||||
import de.pixart.messenger.xmpp.InvalidJid;
|
||||
import de.pixart.messenger.xmpp.OnIqPacketReceived;
|
||||
import de.pixart.messenger.xmpp.OnUpdateBlocklist;
|
||||
import de.pixart.messenger.xmpp.forms.Data;
|
||||
import de.pixart.messenger.xmpp.stanzas.IqPacket;
|
||||
import rocks.xmpp.addr.Jid;
|
||||
|
||||
|
@ -416,4 +420,55 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<Jid> items(IqPacket packet) {
|
||||
ArrayList<Jid> items = new ArrayList<>();
|
||||
final Element query = packet.findChild("query", Namespace.DISCO_ITEMS);
|
||||
if (query == null) {
|
||||
return items;
|
||||
}
|
||||
for (Element child : query.getChildren()) {
|
||||
if ("item".equals(child.getName())) {
|
||||
Jid jid = child.getAttributeAsJid("jid");
|
||||
if (jid != null) {
|
||||
items.add(jid);
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public static Room parseRoom(IqPacket packet) {
|
||||
final Element query = packet.findChild("query", Namespace.DISCO_INFO);
|
||||
if (query == null) {
|
||||
return null;
|
||||
}
|
||||
final Element x = query.findChild("x");
|
||||
if (x == null) {
|
||||
return null;
|
||||
}
|
||||
final Element identity = query.findChild("identity");
|
||||
Data data = Data.parse(x);
|
||||
String address = packet.getFrom().toEscapedString();
|
||||
String name = identity == null ? null : identity.getAttribute("name");
|
||||
String roomName = data.getValue("muc#roomconfig_roomname");
|
||||
;
|
||||
String description = data.getValue("muc#roominfo_description");
|
||||
String language = data.getValue("muc#roominfo_lang");
|
||||
String occupants = data.getValue("muc#roominfo_occupants");
|
||||
int nusers;
|
||||
try {
|
||||
nusers = occupants == null ? 0 : Integer.parseInt(occupants);
|
||||
} catch (NumberFormatException e) {
|
||||
nusers = 0;
|
||||
}
|
||||
|
||||
return new Room(
|
||||
address,
|
||||
TextUtils.isEmpty(roomName) ? name : roomName,
|
||||
description,
|
||||
language,
|
||||
nusers
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import java.util.Collections;
|
|||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
|
@ -25,8 +26,10 @@ import de.pixart.messenger.crypto.OtrService;
|
|||
import de.pixart.messenger.crypto.axolotl.AxolotlService;
|
||||
import de.pixart.messenger.crypto.axolotl.BrokenSessionException;
|
||||
import de.pixart.messenger.crypto.axolotl.NotEncryptedForThisDeviceException;
|
||||
import de.pixart.messenger.crypto.axolotl.OutdatedSenderException;
|
||||
import de.pixart.messenger.crypto.axolotl.XmppAxolotlMessage;
|
||||
import de.pixart.messenger.entities.Account;
|
||||
import de.pixart.messenger.entities.Bookmark;
|
||||
import de.pixart.messenger.entities.Contact;
|
||||
import de.pixart.messenger.entities.Conversation;
|
||||
import de.pixart.messenger.entities.Conversational;
|
||||
|
@ -43,6 +46,7 @@ import de.pixart.messenger.services.XmppConnectionService;
|
|||
import de.pixart.messenger.utils.CryptoHelper;
|
||||
import de.pixart.messenger.utils.Namespace;
|
||||
import de.pixart.messenger.xml.Element;
|
||||
import de.pixart.messenger.xml.LocalizedContent;
|
||||
import de.pixart.messenger.xmpp.InvalidJid;
|
||||
import de.pixart.messenger.xmpp.OnMessagePacketReceived;
|
||||
import de.pixart.messenger.xmpp.chatstate.ChatState;
|
||||
|
@ -50,6 +54,8 @@ import de.pixart.messenger.xmpp.pep.Avatar;
|
|||
import de.pixart.messenger.xmpp.stanzas.MessagePacket;
|
||||
import rocks.xmpp.addr.Jid;
|
||||
|
||||
import static de.pixart.messenger.entities.Message.DELETED_MESSAGE_BODY;
|
||||
|
||||
public class MessageParser extends AbstractParser implements OnMessagePacketReceived {
|
||||
|
||||
private static final List<String> CLIENTS_SENDING_HTML_IN_OTR = Arrays.asList("Pidgin", "Adium", "Trillian");
|
||||
|
@ -202,7 +208,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
}
|
||||
}
|
||||
|
||||
private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, boolean checkedForDuplicates, boolean postpone) {
|
||||
private Message parseAxolotlChat(Element axolotlMessage, Jid from, Conversation conversation, int status, final boolean checkedForDuplicates, boolean postpone) {
|
||||
final AxolotlService service = conversation.getAccount().getAxolotlService();
|
||||
final XmppAxolotlMessage xmppAxolotlMessage;
|
||||
try {
|
||||
|
@ -217,14 +223,21 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage, postpone);
|
||||
} catch (BrokenSessionException e) {
|
||||
if (checkedForDuplicates) {
|
||||
service.reportBrokenSessionException(e, postpone);
|
||||
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
|
||||
if (service.trustedOrPreviouslyResponded(from.asBareJid())) {
|
||||
service.reportBrokenSessionException(e, postpone);
|
||||
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "ignoring broken session exception because contact was not trusted");
|
||||
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, "ignoring broken session exception because checkForDuplicase failed");
|
||||
Log.d(Config.LOGTAG, "ignoring broken session exception because checkForDuplicates failed");
|
||||
return null;
|
||||
}
|
||||
} catch (NotEncryptedForThisDeviceException e) {
|
||||
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE, status);
|
||||
} catch (OutdatedSenderException e) {
|
||||
return new Message(conversation, "", Message.ENCRYPTION_AXOLOTL_FAILED, status);
|
||||
}
|
||||
if (plaintextMessage != null) {
|
||||
Message finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
|
||||
|
@ -239,31 +252,28 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
return null;
|
||||
}
|
||||
|
||||
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");
|
||||
private Invite extractInvite(Element message) {
|
||||
final Element mucUser = message.findChild("x", Namespace.MUC_USER);
|
||||
if (mucUser != null) {
|
||||
Element invite = mucUser.findChild("invite");
|
||||
if (invite != null) {
|
||||
String password = x.findChildContent("password");
|
||||
String password = mucUser.findChildContent("password");
|
||||
Jid from = InvalidJid.getNullForInvalid(invite.getAttributeAsJid("from"));
|
||||
Contact contact = from == null ? null : account.getRoster().getContact(from);
|
||||
Jid room = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
|
||||
if (room == null) {
|
||||
return null;
|
||||
}
|
||||
return new Invite(room, password, contact);
|
||||
return new Invite(room, password, false, from);
|
||||
}
|
||||
} else {
|
||||
x = message.findChild("x", "jabber:x:conference");
|
||||
if (x != null) {
|
||||
Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
|
||||
Contact contact = from == null ? null : account.getRoster().getContact(from);
|
||||
Jid room = InvalidJid.getNullForInvalid(x.getAttributeAsJid("jid"));
|
||||
if (room == null) {
|
||||
return null;
|
||||
}
|
||||
return new Invite(room, x.getAttribute("password"), contact);
|
||||
}
|
||||
final Element conference = message.findChild("x", "jabber:x:conference");
|
||||
if (conference != null) {
|
||||
Jid from = InvalidJid.getNullForInvalid(message.getAttributeAsJid("from"));
|
||||
Jid room = InvalidJid.getNullForInvalid(conference.getAttributeAsJid("jid"));
|
||||
if (room == null) {
|
||||
return null;
|
||||
}
|
||||
return new Invite(room, conference.getAttribute("password"), true, from);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -313,25 +323,55 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
if (account.getXmppConnection().getFeatures().bookmarksConversion()) {
|
||||
final Element i = items.findChild("item");
|
||||
final Element storage = i == null ? null : i.findChild("storage", Namespace.BOOKMARKS);
|
||||
new Thread(() -> {
|
||||
mXmppConnectionService.processBookmarks(account, storage, true);
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing bookmark PEP event");
|
||||
}).start();
|
||||
Map<Jid, Bookmark> bookmarks = Bookmark.parseFromStorage(storage, account);
|
||||
mXmppConnectionService.processBookmarksInitial(account, bookmarks, true);
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": processing bookmark PEP event");
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring bookmark PEP event because bookmark conversion was not detected");
|
||||
}
|
||||
} else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
|
||||
final Element item = items.findChild("item");
|
||||
final Element retract = items.findChild("retract");
|
||||
if (item != null) {
|
||||
final Bookmark bookmark = Bookmark.parseFromItem(item, account);
|
||||
if (bookmark != null) {
|
||||
account.putBookmark(bookmark);
|
||||
mXmppConnectionService.processModifiedBookmark(bookmark);
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
}
|
||||
if (retract != null) {
|
||||
final Jid id = InvalidJid.getNullForInvalid(retract.getAttributeAsJid("id"));
|
||||
if (id != null) {
|
||||
account.removeBookmark(id);
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmark for " + id);
|
||||
mXmppConnectionService.processDeletedBookmark(account, id);
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + " received pubsub notification for node=" + node);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseDeleteEvent(final Element event, final Jid from, final Account account) {
|
||||
final Element delete = event.findChild("delete");
|
||||
if (delete == null) {
|
||||
return;
|
||||
}
|
||||
String node = delete.getAttribute("node");
|
||||
final String node = delete == null ? null : delete.getAttribute("node");
|
||||
if (Namespace.NICK.equals(node)) {
|
||||
Log.d(Config.LOGTAG, "parsing nick delete event from " + from);
|
||||
setNick(account, from, null);
|
||||
} else if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
|
||||
account.setBookmarks(Collections.emptyMap());
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": deleted bookmarks node");
|
||||
}
|
||||
}
|
||||
|
||||
private void parsePurgeEvent(final Element event, final Jid from, final Account account) {
|
||||
final Element purge = event.findChild("purge");
|
||||
final String node = purge == null ? null : purge.getAttribute("node");
|
||||
if (Namespace.BOOKMARKS2.equals(node) && account.getJid().asBareJid().equals(from)) {
|
||||
account.setBookmarks(Collections.emptyMap());
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": purged bookmarks");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -358,12 +398,12 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
Message.STATUS_SEND_FAILED,
|
||||
extractErrorMessage(packet));
|
||||
final Element error = packet.findChild("error");
|
||||
final boolean notAcceptable = error != null && error.hasChild("not-acceptable");
|
||||
if (notAcceptable) {
|
||||
final boolean pingWorthyError = error != null && (error.hasChild("not-acceptable") || error.hasChild("remote-server-timeout") || error.hasChild("remote-server-not-found"));
|
||||
if (pingWorthyError) {
|
||||
Conversation conversation = mXmppConnectionService.find(account, from);
|
||||
if (conversation != null && conversation.getMode() == Conversational.MODE_MULTI) {
|
||||
if (conversation.getMucOptions().online()) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received not-acceptable error for seemingly online muc at " + from);
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received ping worthy error for seemingly online muc at " + from);
|
||||
mXmppConnectionService.mucSelfPingAndRejoin(conversation);
|
||||
}
|
||||
}
|
||||
|
@ -429,8 +469,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
if (timestamp == null) {
|
||||
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 LocalizedContent body = packet.getBody();
|
||||
final Element mucUserElement = packet.findChild("x", Namespace.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", Namespace.OOB);
|
||||
|
@ -438,7 +478,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
final URL xP1S3url = xP1S3 == null ? null : P1S3UrlStreamHandler.of(xP1S3);
|
||||
final String oobUrl = oob != null ? oob.findChildContent("url") : null;
|
||||
final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id");
|
||||
final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
|
||||
final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
|
||||
int status;
|
||||
final Jid counterpart;
|
||||
final Jid to = packet.getTo();
|
||||
|
@ -479,9 +519,16 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
selfAddressed = false;
|
||||
}
|
||||
|
||||
Invite invite = extractInvite(account, packet);
|
||||
if (invite != null && invite.execute(account)) {
|
||||
return;
|
||||
final Invite invite = extractInvite(packet);
|
||||
if (invite != null) {
|
||||
if (isTypeGroupChat) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring invite to " + invite.jid + " because type=groupchat");
|
||||
} else if (invite.direct && (mucUserElement != null || invite.inviter == null || mXmppConnectionService.isMuc(account, invite.inviter))) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring direct invite to " + invite.jid + " because it was received in MUC");
|
||||
} else {
|
||||
invite.execute(account);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || oobUrl != null || xP1S3 != null) && !isMucStatusMessage) {
|
||||
|
@ -504,13 +551,17 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
if (conversation.getMucOptions().isSelf(counterpart)) {
|
||||
status = Message.STATUS_SEND_RECEIVED;
|
||||
isCarbon = true; //not really carbon but received from another resource
|
||||
//TODO this would be the place to change the body after something like mod_pastebin
|
||||
if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status, serverMsgId)) {
|
||||
return;
|
||||
} else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) {
|
||||
Message message = conversation.findSentMessageWithBody(packet.getBody());
|
||||
if (message != null) {
|
||||
mXmppConnectionService.markMessage(message, status);
|
||||
return;
|
||||
LocalizedContent localizedBody = packet.getBody();
|
||||
if (localizedBody != null) {
|
||||
Message message = conversation.findSentMessageWithBody(localizedBody.content);
|
||||
if (message != null) {
|
||||
mXmppConnectionService.markMessage(message, status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -518,15 +569,18 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
}
|
||||
}
|
||||
final Message message;
|
||||
if (body != null && body.startsWith("?OTR") && Config.supportOtr()) {
|
||||
if (body != null && body.content.startsWith("?OTR") && Config.supportOtr()) {
|
||||
if (!isForwarded && !isTypeGroupChat && isProperlyAddressed && !conversationMultiMode) {
|
||||
message = parseOtrChat(body, from, remoteMsgId, conversation);
|
||||
message = parseOtrChat(body.content, from, remoteMsgId, conversation);
|
||||
if (message == null) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": ignoring OTR message from " + from + " isForwarded=" + Boolean.toString(isForwarded) + ", isProperlyAddressed=" + Boolean.valueOf(isProperlyAddressed));
|
||||
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
|
||||
message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
|
||||
if (body.count > 1) {
|
||||
message.setBodyLanguage(body.language);
|
||||
}
|
||||
}
|
||||
} else if (xP1S3url != null) {
|
||||
message = new Message(conversation, xP1S3url.toString(), Message.ENCRYPTION_NONE, status);
|
||||
|
@ -557,8 +611,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
fallbacksBySourceId = Collections.emptySet();
|
||||
origin = from;
|
||||
}
|
||||
|
||||
final boolean checkedForDuplicates = serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId);
|
||||
final boolean liveMessage = query == null && !isTypeGroupChat && mucUserElement == null;
|
||||
final boolean checkedForDuplicates = liveMessage || (serverMsgId != null && remoteMsgId != null && !conversation.possibleDuplicate(serverMsgId, remoteMsgId));
|
||||
|
||||
if (origin != null) {
|
||||
message = parseAxolotlChat(axolotlEncrypted, origin, conversation, status, checkedForDuplicates, query != null);
|
||||
|
@ -598,7 +652,10 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
}
|
||||
} else {
|
||||
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
|
||||
message = new Message(conversation, body.content, Message.ENCRYPTION_NONE, status);
|
||||
if (body.count > 1) {
|
||||
message.setBodyLanguage(body.language);
|
||||
}
|
||||
}
|
||||
|
||||
message.setCounterpart(counterpart);
|
||||
|
@ -606,7 +663,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
message.setServerMsgId(serverMsgId);
|
||||
message.setCarbon(isCarbon);
|
||||
message.setTime(timestamp);
|
||||
if (body != null && body.equals(oobUrl)) {
|
||||
if (body != null && body.content != null && body.content.equals(oobUrl)) {
|
||||
message.setOob(true);
|
||||
if (CryptoHelper.isPgpEncryptedUrl(oobUrl)) {
|
||||
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
|
||||
|
@ -650,7 +707,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
final boolean fingerprintsMatch = replacedMessage.getFingerprint() == null
|
||||
|| replacedMessage.getFingerprint().equals(message.getFingerprint());
|
||||
final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null
|
||||
&& replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart());
|
||||
&& message.getTrueCounterpart() != null
|
||||
&& replacedMessage.getTrueCounterpart().asBareJid().equals(message.getTrueCounterpart().asBareJid());
|
||||
final boolean mucUserMatches = query == null && replacedMessage.sameMucUser(message); //can not be checked when using mam
|
||||
final boolean duplicate = conversation.hasDuplicateMessage(message);
|
||||
if (fingerprintsMatch && (trueCountersMatch || !conversationMultiMode || mucUserMatches) && !duplicate) {
|
||||
|
@ -659,7 +717,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
final String uuid = replacedMessage.getUuid();
|
||||
replacedMessage.setUuid(UUID.randomUUID().toString());
|
||||
replacedMessage.setBody(message.getBody());
|
||||
replacedMessage.setEdited(replacedMessage.getRemoteMsgId());
|
||||
replacedMessage.putEdited(replacedMessage.getRemoteMsgId(), replacedMessage.getServerMsgId());
|
||||
replacedMessage.setRemoteMsgId(remoteMsgId);
|
||||
if (replacedMessage.getServerMsgId() == null || message.getServerMsgId() != null) {
|
||||
replacedMessage.setServerMsgId(message.getServerMsgId());
|
||||
|
@ -689,6 +747,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received message correction but verification didn't check out");
|
||||
}
|
||||
}
|
||||
} else if (replacementId != null && !mXmppConnectionService.allowMessageCorrection() && message.getBody().equals(DELETED_MESSAGE_BODY)) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": received deleted message but LMC is deactivated");
|
||||
return;
|
||||
}
|
||||
|
||||
long deletionDate = mXmppConnectionService.getAutomaticMessageDeletionDate();
|
||||
|
@ -719,7 +780,7 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
} else {
|
||||
serverMsgIdUpdated = false;
|
||||
}
|
||||
Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + Boolean.toString(serverMsgIdUpdated));
|
||||
Log.d(Config.LOGTAG, "skipping duplicate message with " + message.getCounterpart() + ". serverMsgIdUpdated=" + serverMsgIdUpdated);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -812,11 +873,11 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
if (isTypeGroupChat) {
|
||||
if (packet.hasChild("subject")) {
|
||||
if (packet.hasChild("subject")) { //TODO usually we would want to check for lack of body; however some servers do set a body :(
|
||||
if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) {
|
||||
conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0);
|
||||
String subject = packet.findInternationalizedChildContent("subject");
|
||||
if (conversation.getMucOptions().setSubject(subject)) {
|
||||
final LocalizedContent subject = packet.findInternationalizedChildContentInDefaultNamespace("subject");
|
||||
if (subject != null && conversation.getMucOptions().setSubject(subject.content)) {
|
||||
mXmppConnectionService.updateConversation(conversation);
|
||||
}
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
|
@ -889,6 +950,9 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
final Jid sender = InvalidJid.getNullForInvalid(displayed.getAttributeAsJid("sender"));
|
||||
if (packet.fromAccount(account) && !selfAddressed) {
|
||||
dismissNotification(account, counterpart, query);
|
||||
if (query == null) {
|
||||
activateGracePeriod(account);
|
||||
}
|
||||
} else if (isTypeGroupChat) {
|
||||
Conversation conversation = mXmppConnectionService.find(account, counterpart.asBareJid());
|
||||
if (conversation != null && id != null && sender != null) {
|
||||
|
@ -902,9 +966,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
mXmppConnectionService.markRead(conversation);
|
||||
}
|
||||
} else if (!counterpart.isBareJid() && trueJid != null) {
|
||||
ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
|
||||
final ReadByMarker readByMarker = ReadByMarker.from(counterpart, trueJid);
|
||||
if (message.addReadByMarker(readByMarker)) {
|
||||
Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": added read by (" + readByMarker.getRealJid() + ") to message '" + message.getBody() + "'");
|
||||
mXmppConnectionService.updateMessage(message, false);
|
||||
updateReadMarker(account, from, id, selfAddressed, counterpart, query);
|
||||
}
|
||||
|
@ -933,6 +996,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
parseEvent(event, original.getFrom(), account);
|
||||
} else if (event.hasChild("delete")) {
|
||||
parseDeleteEvent(event, original.getFrom(), account);
|
||||
} else if (event.hasChild("purge")) {
|
||||
parsePurgeEvent(event, original.getFrom(), account);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1000,11 +1065,13 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
private class Invite {
|
||||
final Jid jid;
|
||||
final String password;
|
||||
final Contact inviter;
|
||||
final boolean direct;
|
||||
final Jid inviter;
|
||||
|
||||
Invite(Jid jid, String password, Contact inviter) {
|
||||
Invite(Jid jid, String password, boolean direct, Jid inviter) {
|
||||
this.jid = jid;
|
||||
this.password = password;
|
||||
this.direct = direct;
|
||||
this.inviter = inviter;
|
||||
}
|
||||
|
||||
|
@ -1017,7 +1084,8 @@ public class MessageParser extends AbstractParser implements OnMessagePacketRece
|
|||
} else {
|
||||
conversation.getMucOptions().setPassword(password);
|
||||
mXmppConnectionService.databaseBackend.updateConversation(conversation);
|
||||
mXmppConnectionService.joinMuc(conversation, inviter != null && inviter.mutualPresenceSubscription());
|
||||
final Contact contact = inviter != null ? account.getRoster().getContactFromContactList(inviter) : null;
|
||||
mXmppConnectionService.joinMuc(conversation, contact != null && contact.mutualPresenceSubscription());
|
||||
mXmppConnectionService.updateConversationUi();
|
||||
}
|
||||
return true;
|
||||
|
|
|
@ -59,7 +59,7 @@ public class PresenceParser extends AbstractParser implements
|
|||
final Jid from = packet.getFrom();
|
||||
if (!from.isBareJid()) {
|
||||
final String type = packet.getAttribute("type");
|
||||
final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user");
|
||||
final Element x = packet.findChild("x", Namespace.MUC_USER);
|
||||
Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update"));
|
||||
final List<String> codes = getStatusCodes(x);
|
||||
if (type == null) {
|
||||
|
@ -191,7 +191,7 @@ public class PresenceParser extends AbstractParser implements
|
|||
final Jid alternate;
|
||||
if (gone != null) {
|
||||
final XmppUri xmppUri = new XmppUri(gone);
|
||||
if (xmppUri.isJidValid()) {
|
||||
if (xmppUri.isValidJid()) {
|
||||
alternate = xmppUri.getJid();
|
||||
} else {
|
||||
alternate = null;
|
||||
|
@ -361,7 +361,7 @@ public class PresenceParser extends AbstractParser implements
|
|||
|
||||
@Override
|
||||
public void onPresencePacketReceived(Account account, PresencePacket packet) {
|
||||
if (packet.hasChild("x", "http://jabber.org/protocol/muc#user")) {
|
||||
if (packet.hasChild("x", Namespace.MUC_USER)) {
|
||||
this.parseConferencePresence(packet, account);
|
||||
} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
|
||||
this.parseConferencePresence(packet, account);
|
||||
|
|
|
@ -28,14 +28,11 @@ import java.security.cert.CertificateEncodingException;
|
|||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
@ -54,6 +51,7 @@ import de.pixart.messenger.entities.Roster;
|
|||
import de.pixart.messenger.entities.ServiceDiscoveryResult;
|
||||
import de.pixart.messenger.services.ShortcutService;
|
||||
import de.pixart.messenger.utils.CryptoHelper;
|
||||
import de.pixart.messenger.utils.CursorUtils;
|
||||
import de.pixart.messenger.utils.FtsUtils;
|
||||
import de.pixart.messenger.utils.Resolver;
|
||||
import de.pixart.messenger.xmpp.InvalidJid;
|
||||
|
@ -63,7 +61,7 @@ import rocks.xmpp.addr.Jid;
|
|||
public class DatabaseBackend extends SQLiteOpenHelper {
|
||||
|
||||
public static final String DATABASE_NAME = "history";
|
||||
public static final int DATABASE_VERSION = 48; // = Conversations DATABASE_VERSION + 4
|
||||
public static final int DATABASE_VERSION = 51; // = Conversations DATABASE_VERSION + 5
|
||||
private static DatabaseBackend instance = null;
|
||||
|
||||
private static String CREATE_CONTATCS_STATEMENT = "create table "
|
||||
|
@ -152,6 +150,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ ");";
|
||||
|
||||
private static String RESOLVER_RESULTS_TABLENAME = "resolver_results";
|
||||
|
||||
private static String CREATE_RESOLVER_RESULTS_TABLE = "create table " + RESOLVER_RESULTS_TABLENAME + "("
|
||||
+ Resolver.Result.DOMAIN + " TEXT,"
|
||||
+ Resolver.Result.HOSTNAME + " TEXT,"
|
||||
|
@ -160,8 +159,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ Resolver.Result.DIRECT_TLS + " NUMBER,"
|
||||
+ Resolver.Result.AUTHENTICATED + " NUMBER,"
|
||||
+ Resolver.Result.PORT + " NUMBER,"
|
||||
+ Resolver.Result.TIME_REQUESTED + " NUMBER,"
|
||||
+ "UNIQUE(" + Resolver.Result.DOMAIN + ") ON CONFLICT REPLACE"
|
||||
+ ");";
|
||||
|
||||
private static String CREATE_MESSAGE_TIME_INDEX = "create INDEX message_time_index ON " + Message.TABLENAME + "(" + Message.TIME_SENT + ")";
|
||||
private static String CREATE_MESSAGE_CONVERSATION_INDEX = "create INDEX message_conversation_index ON " + Message.TABLENAME + "(" + Message.CONVERSATION + ")";
|
||||
private static String CREATE_MESSAGE_DELETED_INDEX = "create index message_deleted_index ON " + Message.TABLENAME + "(" + Message.DELETED + ")";
|
||||
|
@ -195,7 +196,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
@Override
|
||||
public void onConfigure(SQLiteDatabase db) {
|
||||
db.execSQL("PRAGMA foreign_keys=ON");
|
||||
db.rawQuery("PRAGMA secure_delete=ON", null);
|
||||
db.rawQuery("PRAGMA secure_delete=ON", null).close();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -241,6 +242,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
+ Message.READ_BY_MARKERS + " TEXT,"
|
||||
+ Message.MARKABLE + " NUMBER DEFAULT 0,"
|
||||
+ Message.FILE_DELETED + " NUMBER DEFAULT 0,"
|
||||
+ Message.BODY_LANGUAGE + " TEXT,"
|
||||
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
|
||||
+ Message.CONVERSATION + ") REFERENCES "
|
||||
+ Conversation.TABLENAME + "(" + Conversation.UUID
|
||||
|
@ -529,10 +531,6 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.MARKABLE + " NUMBER DEFAULT 0");
|
||||
}
|
||||
|
||||
if (oldVersion < 40 && newVersion >= 40) {
|
||||
db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
|
||||
}
|
||||
|
||||
if (oldVersion < 42 && newVersion >= 42) {
|
||||
db.execSQL(CREATE_MESSAGE_INDEX_TABLE);
|
||||
db.execSQL(CREATE_MESSAGE_INSERT_TRIGGER);
|
||||
|
@ -562,12 +560,23 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
}
|
||||
}
|
||||
|
||||
if (oldVersion < 48 && newVersion >= 48) {
|
||||
try {
|
||||
db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
|
||||
} catch (Exception e) {
|
||||
//ignore
|
||||
}
|
||||
if (oldVersion < 49 && newVersion >= 49) {
|
||||
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.BODY_LANGUAGE);
|
||||
}
|
||||
|
||||
if (oldVersion < 50 && newVersion >= 50) {
|
||||
final long start = SystemClock.elapsedRealtime();
|
||||
db.rawQuery("PRAGMA secure_delete = FALSE", null).close();
|
||||
db.execSQL("update " + Message.TABLENAME + " set " + Message.EDITED + "=NULL");
|
||||
db.rawQuery("PRAGMA secure_delete=ON", null).close();
|
||||
final long diff = SystemClock.elapsedRealtime() - start;
|
||||
Log.d(Config.LOGTAG, "deleted old edit information in " + diff + "ms");
|
||||
}
|
||||
|
||||
if (oldVersion < 51 && newVersion >= 51) {
|
||||
// values in resolver_result are cache and not worth to store
|
||||
db.execSQL("DROP TABLE IF EXISTS " + RESOLVER_RESULTS_TABLENAME);
|
||||
db.execSQL(CREATE_RESOLVER_RESULTS_TABLE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -601,7 +610,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
String[] updateArgs = {
|
||||
newJid,
|
||||
cursor.getString(cursor.getColumnIndex(Conversation.UUID)),
|
||||
};
|
||||
|
@ -617,14 +626,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
String newJid;
|
||||
try {
|
||||
newJid = Jid.of(cursor.getString(cursor.getColumnIndex(Contact.JID))).toString();
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
} catch (final IllegalArgumentException e) {
|
||||
Log.e(Config.LOGTAG, "Failed to migrate Contact JID "
|
||||
+ cursor.getString(cursor.getColumnIndex(Contact.JID))
|
||||
+ ": " + ignored + ". Skipping...");
|
||||
+ ": Skipping...", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
final String[] updateArgs = {
|
||||
newJid,
|
||||
cursor.getString(cursor.getColumnIndex(Contact.ACCOUNT)),
|
||||
cursor.getString(cursor.getColumnIndex(Contact.JID)),
|
||||
|
@ -653,7 +662,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
continue;
|
||||
}
|
||||
|
||||
String updateArgs[] = {
|
||||
String[] updateArgs = {
|
||||
newServer,
|
||||
cursor.getString(cursor.getColumnIndex(Account.UUID)),
|
||||
};
|
||||
|
@ -791,12 +800,10 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
null, null, Message.TIME_SENT + " DESC",
|
||||
String.valueOf(limit));
|
||||
}
|
||||
CursorUtils.upgradeCursorWindowSize(cursor);
|
||||
while (cursor.moveToNext()) {
|
||||
try {
|
||||
final Message message = Message.fromCursor(cursor, conversation);
|
||||
if (message != null && !message.isMessageDeleted()) {
|
||||
list.add(0, message);
|
||||
}
|
||||
list.add(0, Message.fromCursor(cursor, conversation));
|
||||
} catch (Exception e) {
|
||||
Log.e(Config.LOGTAG, "unable to restore message");
|
||||
}
|
||||
|
@ -1009,12 +1016,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
|
||||
private List<Account> getAccounts(SQLiteDatabase db) {
|
||||
List<Account> list = new ArrayList<>();
|
||||
Cursor cursor = db.query(Account.TABLENAME, null, null, null, null,
|
||||
null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
list.add(Account.fromCursor(cursor));
|
||||
try (Cursor cursor = db.query(Account.TABLENAME, null, null, null, null,
|
||||
null, null)) {
|
||||
while (cursor.moveToNext()) {
|
||||
list.add(Account.fromCursor(cursor));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
cursor.close();
|
||||
return list;
|
||||
}
|
||||
|
||||
|
@ -1052,7 +1061,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
|
|||
public void readRoster(Roster roster) {
|
||||
SQLiteDatabase db = this.getReadableDatabase();
|
||||
Cursor cursor;
|
||||
String args[] = {roster.getAccount().getUuid()};
|
||||
String[] args = {roster.getAccount().getUuid()};
|
||||
cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT + "=?", args, null, null, null);
|
||||
while (cursor.moveToNext()) {
|
||||
roster.initContact(Contact.fromCursor(cursor));
|
||||
|
|
|
@ -13,6 +13,8 @@ import android.graphics.Color;
|
|||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.pdf.PdfRenderer;
|
||||
import android.media.MediaMetadataRetriever;
|
||||
import android.media.MediaScannerConnection;
|
||||
import android.net.Uri;
|
||||
|
@ -21,15 +23,17 @@ import android.os.Environment;
|
|||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.annotation.RequiresApi;
|
||||
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.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.content.FileProvider;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
|
@ -41,6 +45,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.net.URL;
|
||||
import java.security.DigestOutputStream;
|
||||
|
@ -57,8 +62,10 @@ import de.pixart.messenger.Config;
|
|||
import de.pixart.messenger.R;
|
||||
import de.pixart.messenger.entities.DownloadableFile;
|
||||
import de.pixart.messenger.entities.Message;
|
||||
import de.pixart.messenger.services.AttachFileToConversationRunnable;
|
||||
import de.pixart.messenger.services.XmppConnectionService;
|
||||
import de.pixart.messenger.ui.util.Attachment;
|
||||
import de.pixart.messenger.utils.Compatibility;
|
||||
import de.pixart.messenger.utils.CryptoHelper;
|
||||
import de.pixart.messenger.utils.ExifHelper;
|
||||
import de.pixart.messenger.utils.FileUtils;
|
||||
|
@ -75,6 +82,7 @@ public class FileBackend {
|
|||
private static final SimpleDateFormat fileDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmssSSS", Locale.US);
|
||||
|
||||
private static final String FILE_PROVIDER = ".files";
|
||||
private static final String APP_DIRECTORY = "Pix-Art Messenger";
|
||||
|
||||
private XmppConnectionService mXmppConnectionService;
|
||||
|
||||
|
@ -268,6 +276,7 @@ public class FileBackend {
|
|||
}
|
||||
|
||||
public static boolean allFilesUnderSize(Context context, List<Attachment> attachments, long max) {
|
||||
final boolean compressVideo = !AttachFileToConversationRunnable.getVideoCompression(context).equals("uncompressed");
|
||||
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
|
||||
|
@ -277,7 +286,7 @@ public class FileBackend {
|
|||
continue;
|
||||
}
|
||||
String mime = attachment.getMime();
|
||||
if (mime != null && mime.startsWith("video/")) {
|
||||
if (mime != null && mime.startsWith("video/") && compressVideo) {
|
||||
try {
|
||||
Dimensions dimensions = FileBackend.getVideoDimensions(context, attachment.getUri());
|
||||
if (dimensions.getMin() >= 720) {
|
||||
|
@ -296,12 +305,12 @@ public class FileBackend {
|
|||
return true;
|
||||
}
|
||||
|
||||
public List<Attachment> convertToAttachments(List<DatabaseBackend.FilePath> relativeFilePaths) {
|
||||
List<Attachment> attachments = new ArrayList<>();
|
||||
public List<Attachment> convertToAttachments(final List<DatabaseBackend.FilePath> relativeFilePaths) {
|
||||
final List<Attachment> attachments = new ArrayList<>();
|
||||
for (DatabaseBackend.FilePath relativeFilePath : relativeFilePaths) {
|
||||
final String mime = MimeUtils.guessMimeTypeFromExtension(MimeUtils.extractRelevantExtension(relativeFilePath.path));
|
||||
final File file = getFileForPath(relativeFilePath.path, mime);
|
||||
if (file.exists() && mime != null && (mime.startsWith("image/") || mime.startsWith("video/"))) {
|
||||
if (file.exists()) {
|
||||
attachments.add(Attachment.of(relativeFilePath.uuid, file, mime));
|
||||
}
|
||||
}
|
||||
|
@ -310,26 +319,26 @@ public class FileBackend {
|
|||
|
||||
public static String getConversationsDirectory(final String type) {
|
||||
if (type.equalsIgnoreCase("null")) {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/";
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator;
|
||||
} else {
|
||||
return getAppMediaDirectory() + "Pix-Art Messenger" + " " + type + "/";
|
||||
return getAppMediaDirectory() + APP_DIRECTORY + " " + type + File.separator;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getAppMediaDirectory() {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/Media/";
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Media" + File.separator;
|
||||
}
|
||||
|
||||
public static String getBackupDirectory() {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/Database/";
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Database" + File.separator;
|
||||
}
|
||||
|
||||
public static String getAppLogsDirectory() {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/Chats/";
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Chats" + File.separator;
|
||||
}
|
||||
|
||||
public static String getAppUpdateDirectory() {
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + "Pix-Art Messenger" + "/Update/";
|
||||
return Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + APP_DIRECTORY + File.separator + "Update" + File.separator;
|
||||
}
|
||||
|
||||
private Bitmap resize(final Bitmap originalBitmap, int size) throws IOException {
|
||||
|
@ -643,9 +652,11 @@ public class FileBackend {
|
|||
}
|
||||
DownloadableFile file = getFile(message);
|
||||
final String mime = file.getMimeType();
|
||||
if (mime.startsWith("video/")) {
|
||||
if ("application/pdf".equals(mime) && Compatibility.runsTwentyOne()) {
|
||||
thumbnail = getPDFPreview(file, size);
|
||||
} else if (mime.startsWith("video/")) {
|
||||
thumbnail = getVideoPreview(file, size);
|
||||
} else {
|
||||
} else if (mime.startsWith("image/")) {
|
||||
Bitmap fullsize = getFullsizeImagePreview(file, size);
|
||||
if (fullsize == null) {
|
||||
throw new FileNotFoundException();
|
||||
|
@ -665,6 +676,46 @@ public class FileBackend {
|
|||
return thumbnail;
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private Bitmap getPDFPreview(final File file, int size) {
|
||||
try {
|
||||
final ParcelFileDescriptor mFileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
if (mFileDescriptor == null) {
|
||||
return null;
|
||||
}
|
||||
final PdfRenderer renderer = new PdfRenderer(mFileDescriptor);
|
||||
final PdfRenderer.Page page = renderer.openPage(0);
|
||||
final Dimensions dimensions = scalePdfDimensions(new Dimensions(page.getHeight(), page.getWidth()));
|
||||
final Bitmap bitmap = Bitmap.createBitmap(dimensions.width, dimensions.height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.eraseColor(Color.WHITE);
|
||||
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
|
||||
drawOverlay(bitmap, R.drawable.show_pdf, 0.75f);
|
||||
page.close();
|
||||
renderer.close();
|
||||
return bitmap;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
final Bitmap placeholder = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
placeholder.eraseColor(Color.WHITE);
|
||||
drawOverlay(placeholder, R.drawable.show_pdf, 0.75f);
|
||||
return placeholder;
|
||||
}
|
||||
}
|
||||
|
||||
private Dimensions scalePdfDimensions(final Dimensions dimensions) {
|
||||
final DisplayMetrics displayMetrics = mXmppConnectionService.getResources().getDisplayMetrics();
|
||||
final int target = (int) (displayMetrics.density * 288);
|
||||
final int w, h;
|
||||
if (dimensions.width <= dimensions.height) {
|
||||
w = Math.max((int) (dimensions.width / ((double) dimensions.height / target)), 1);
|
||||
h = target;
|
||||
} else {
|
||||
w = target;
|
||||
h = Math.max((int) (dimensions.height / ((double) dimensions.width / target)), 1);
|
||||
}
|
||||
return new Dimensions(h, w);
|
||||
}
|
||||
|
||||
private Bitmap getFullsizeImagePreview(File file, int size) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize = calcSampleSize(file, size);
|
||||
|
@ -676,17 +727,53 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
private void drawOverlay(Bitmap bitmap, int resource, float factor) {
|
||||
public void drawOverlay(final Bitmap bitmap, final int resource, final float factor) {
|
||||
drawOverlay(bitmap, resource, factor, false);
|
||||
}
|
||||
|
||||
public void drawOverlay(final Bitmap bitmap, final int resource, final float factor, final boolean corner) {
|
||||
Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
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;
|
||||
float top;
|
||||
if (corner) {
|
||||
left = canvas.getWidth() - targetSize;
|
||||
top = canvas.getHeight() - targetSize;
|
||||
} else {
|
||||
left = (canvas.getWidth() - targetSize) / 2.0f;
|
||||
top = (canvas.getHeight() - targetSize) / 2.0f;
|
||||
}
|
||||
RectF dst = new RectF(left, top, left + targetSize - 1, top + targetSize - 1);
|
||||
canvas.drawBitmap(overlay, null, dst, createAntiAliasingPaint());
|
||||
}
|
||||
|
||||
public void drawOverlayFromDrawable(final Drawable drawable, final int resource, final float factor) {
|
||||
Bitmap overlay = BitmapFactory.decodeResource(mXmppConnectionService.getResources(), resource);
|
||||
Bitmap original = drawableToBitmap(drawable);
|
||||
Canvas canvas = new Canvas(original);
|
||||
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, createAntiAliasingPaint());
|
||||
}
|
||||
|
||||
private static Bitmap drawableToBitmap(Drawable drawable) {
|
||||
Bitmap bitmap = null;
|
||||
if (drawable.getIntrinsicWidth() <= 0 || drawable.getIntrinsicHeight() <= 0) {
|
||||
bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); // Single color bitmap will be created of 1x1 pixel
|
||||
} else {
|
||||
bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
|
||||
}
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
drawable.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private static Paint createAntiAliasingPaint() {
|
||||
Paint paint = new Paint();
|
||||
paint.setAntiAlias(true);
|
||||
|
@ -710,15 +797,18 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
private Bitmap getVideoPreview(File file, int size) throws IOException {
|
||||
MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
|
||||
private Bitmap getVideoPreview(final File file, final int size) {
|
||||
final MediaMetadataRetriever metadataRetriever = new MediaMetadataRetriever();
|
||||
Bitmap frame;
|
||||
try {
|
||||
metadataRetriever.setDataSource(file.getAbsolutePath());
|
||||
frame = metadataRetriever.getFrameAtTime(0);
|
||||
metadataRetriever.release();
|
||||
frame = resize(frame, size);
|
||||
} catch (IOException | RuntimeException e) {
|
||||
} catch (IOException e) {
|
||||
frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
frame.eraseColor(0xff000000);
|
||||
} catch (RuntimeException e) {
|
||||
frame = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
|
||||
frame.eraseColor(0xff000000);
|
||||
}
|
||||
|
@ -731,7 +821,7 @@ public class FileBackend {
|
|||
}
|
||||
|
||||
public Uri getTakePhotoUri() {
|
||||
File file = new File(getTakeFromCameraPath() + "IMG_" + this.fileDateFormat.format(new Date()) + ".jpg");
|
||||
File file = new File(getTakeFromCameraPath() + "IMG_" + fileDateFormat.format(new Date()) + ".jpg");
|
||||
file.getParentFile().mkdirs();
|
||||
return getUriForFile(mXmppConnectionService, file);
|
||||
}
|
||||
|
@ -757,7 +847,7 @@ public class FileBackend {
|
|||
}
|
||||
|
||||
public Uri getTakeVideoUri() {
|
||||
File file = new File(getTakeFromCameraPath() + "VID_" + this.fileDateFormat.format(new Date()) + ".mp4");
|
||||
File file = new File(getTakeFromCameraPath() + "VID_" + fileDateFormat.format(new Date()) + ".mp4");
|
||||
file.getParentFile().mkdirs();
|
||||
return getUriForFile(mXmppConnectionService, file);
|
||||
}
|
||||
|
@ -879,7 +969,9 @@ public class FileBackend {
|
|||
avatar.width = options.outWidth;
|
||||
avatar.type = options.outMimeType;
|
||||
return avatar;
|
||||
} catch (NoSuchAlgorithmException | IOException e) {
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return null;
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
} finally {
|
||||
close(is);
|
||||
|
@ -897,7 +989,7 @@ public class FileBackend {
|
|||
file = new File(getAvatarPath(avatar.getFilename()));
|
||||
avatar.size = file.length();
|
||||
} else {
|
||||
file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath() + "/" + UUID.randomUUID().toString());
|
||||
file = new File(mXmppConnectionService.getCacheDir().getAbsolutePath() + File.separator + UUID.randomUUID().toString());
|
||||
if (file.getParentFile().mkdirs()) {
|
||||
Log.d(Config.LOGTAG, "created cache directory");
|
||||
}
|
||||
|
@ -933,7 +1025,11 @@ public class FileBackend {
|
|||
return false;
|
||||
}
|
||||
avatar.size = bytes.length;
|
||||
} catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) {
|
||||
} catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
return false;
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
return false;
|
||||
} finally {
|
||||
close(os);
|
||||
|
@ -969,7 +1065,10 @@ public class FileBackend {
|
|||
input = rotate(input, getRotation(image));
|
||||
return cropCenterSquare(input, size);
|
||||
}
|
||||
} catch (FileNotFoundException | SecurityException e) {
|
||||
} catch (FileNotFoundException e) {
|
||||
Log.d(Config.LOGTAG, "unable to open file " + image.toString(), e);
|
||||
return null;
|
||||
} catch (SecurityException e) {
|
||||
Log.d(Config.LOGTAG, "unable to open file " + image.toString(), e);
|
||||
return null;
|
||||
} finally {
|
||||
|
@ -1085,14 +1184,22 @@ public class FileBackend {
|
|||
final boolean audio = mime != null && mime.startsWith("audio/");
|
||||
final boolean vcard = mime != null && mime.contains("vcard");
|
||||
final boolean apk = mime != null && mime.equals("application/vnd.android.package-archive");
|
||||
final boolean pdf = "application/pdf".equals(mime);
|
||||
final StringBuilder body = new StringBuilder();
|
||||
if (url != null) {
|
||||
body.append(url.toString());
|
||||
}
|
||||
body.append('|').append(file.getSize());
|
||||
if (image || video) {
|
||||
if (image || video || (pdf && Compatibility.runsTwentyOne())) {
|
||||
try {
|
||||
Dimensions dimensions = image ? getImageDimensions(file) : getVideoDimensions(file);
|
||||
final Dimensions dimensions;
|
||||
if (video) {
|
||||
dimensions = getVideoDimensions(file);
|
||||
} else if (pdf && Compatibility.runsTwentyOne()) {
|
||||
dimensions = getPDFDimensions(file);
|
||||
} else {
|
||||
dimensions = getImageDimensions(file);
|
||||
}
|
||||
if (dimensions.valid()) {
|
||||
body.append('|').append(dimensions.width).append('|').append(dimensions.height);
|
||||
}
|
||||
|
@ -1101,7 +1208,7 @@ public class FileBackend {
|
|||
//fall threw
|
||||
}
|
||||
} else if (audio) {
|
||||
body.append("|0|0|").append(getMediaRuntime(file));
|
||||
body.append("|0|0|").append(getMediaRuntime(file)).append('|').append(getAudioTitleArtist(file));
|
||||
} else if (vcard) {
|
||||
body.append("|0|0|0|").append(getVCard(file));
|
||||
} else if (apk) {
|
||||
|
@ -1112,6 +1219,37 @@ public class FileBackend {
|
|||
message.setType(privateMessage ? Message.TYPE_PRIVATE_FILE : (image ? Message.TYPE_IMAGE : Message.TYPE_FILE));
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
|
||||
private Dimensions getPDFDimensions(final File file) {
|
||||
final ParcelFileDescriptor fileDescriptor;
|
||||
try {
|
||||
fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
||||
if (fileDescriptor == null) {
|
||||
return new Dimensions(0, 0);
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
return new Dimensions(0, 0);
|
||||
}
|
||||
try {
|
||||
final PdfRenderer pdfRenderer = new PdfRenderer(fileDescriptor);
|
||||
final PdfRenderer.Page page = pdfRenderer.openPage(0);
|
||||
final int height = page.getHeight();
|
||||
final int width = page.getWidth();
|
||||
page.close();
|
||||
pdfRenderer.close();
|
||||
return scalePdfDimensions(new Dimensions(height, width));
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG, "unable to get dimensions for pdf document", e);
|
||||
return new Dimensions(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public static void updateFileParams(Message message, URL url, long size) {
|
||||
final StringBuilder body = new StringBuilder();
|
||||
body.append(url.toString()).append('|').append(size);
|
||||
message.setBody(body.toString());
|
||||
}
|
||||
|
||||
private int getMediaRuntime(File file) {
|
||||
try {
|
||||
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
|
||||
|
@ -1122,6 +1260,47 @@ public class FileBackend {
|
|||
}
|
||||
}
|
||||
|
||||
private String getAudioTitleArtist(final File file) {
|
||||
String artist;
|
||||
String title;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try {
|
||||
MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
|
||||
mediaMetadataRetriever.setDataSource(file.toString());
|
||||
artist = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST);
|
||||
if (artist == null) {
|
||||
artist = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST);
|
||||
}
|
||||
if (artist == null) {
|
||||
artist = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER);
|
||||
}
|
||||
title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE);
|
||||
mediaMetadataRetriever.release();
|
||||
boolean separator = false;
|
||||
if (artist != null && artist.length() > 0) {
|
||||
builder.append(artist);
|
||||
separator = true;
|
||||
}
|
||||
if (title != null && title.length() > 0) {
|
||||
if (separator) {
|
||||
builder.append(" - ");
|
||||
}
|
||||
builder.append(title);
|
||||
}
|
||||
try {
|
||||
final String s = builder.substring(0, Math.min(128, builder.length()));
|
||||
final byte[] data = s.trim().getBytes("UTF-8");
|
||||
return Base64.encodeToString(data, Base64.DEFAULT);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private String getAPK(File file, Context context) {
|
||||
String APKName;
|
||||
final PackageManager pm = context.getPackageManager();
|
||||
|
@ -1204,7 +1383,10 @@ public class FileBackend {
|
|||
if (bitmap != null || cacheOnly) {
|
||||
return bitmap;
|
||||
}
|
||||
if (attachment.getMime() != null && attachment.getMime().startsWith("video/")) {
|
||||
DownloadableFile file = new DownloadableFile(attachment.getUri().getPath());
|
||||
if ("application/pdf".equals(attachment.getMime()) && Compatibility.runsTwentyOne()) {
|
||||
bitmap = cropCenterSquare(getPDFPreview(file, size), size);
|
||||
} else if (attachment.getMime() != null && attachment.getMime().startsWith("video/")) {
|
||||
bitmap = cropCenterSquareVideo(attachment.getUri(), size);
|
||||
drawOverlay(bitmap, R.drawable.play_video, 0.75f);
|
||||
} else {
|
||||
|
@ -1351,24 +1533,35 @@ public class FileBackend {
|
|||
return getFile(message).exists();
|
||||
}
|
||||
|
||||
public static void close(Closeable stream) {
|
||||
public static void close(final Closeable stream) {
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "unable to close stream", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void close(Socket socket) {
|
||||
public static void close(final Socket socket) {
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG, "unable to close socket", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void close(final ServerSocket socket) {
|
||||
if (socket != null) {
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
Log.d(Config.LOGTAG, "unable to close socket", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean weOwnFile(Context context, Uri uri) {
|
||||
if (uri == null || !ContentResolver.SCHEME_FILE.equals(uri.getScheme())) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
package de.pixart.messenger.persistance;
|
||||
|
||||
public interface OnPhoneContactsMerged {
|
||||
public void phoneContactsMerged();
|
||||
void phoneContactsMerged();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,14 @@ import android.os.PowerManager;
|
|||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import org.bouncycastle.crypto.engines.AESEngine;
|
||||
import org.bouncycastle.crypto.io.CipherInputStream;
|
||||
import org.bouncycastle.crypto.io.CipherOutputStream;
|
||||
import org.bouncycastle.crypto.modes.AEADBlockCipher;
|
||||
import org.bouncycastle.crypto.modes.GCMBlockCipher;
|
||||
import org.bouncycastle.crypto.params.AEADParameters;
|
||||
import org.bouncycastle.crypto.params.KeyParameter;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
|
@ -15,12 +23,7 @@ import java.security.NoSuchAlgorithmException;
|
|||
import java.security.NoSuchProviderException;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.CipherInputStream;
|
||||
import javax.crypto.CipherOutputStream;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import de.pixart.messenger.Config;
|
||||
import de.pixart.messenger.R;
|
||||
|
@ -28,10 +31,7 @@ import de.pixart.messenger.entities.DownloadableFile;
|
|||
import de.pixart.messenger.utils.Compatibility;
|
||||
|
||||
public class AbstractConnectionManager {
|
||||
private static final String KEYTYPE = "AES";
|
||||
private static final String CIPHERMODE = "AES/GCM/NoPadding";
|
||||
private static final String PROVIDER = "BC";
|
||||
private static final int UI_REFRESH_THRESHOLD = 250;
|
||||
private static final int UI_REFRESH_THRESHOLD = Config.REFRESH_UI_INTERVAL;
|
||||
private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0);
|
||||
protected XmppConnectionService mXmppConnectionService;
|
||||
|
||||
|
@ -41,34 +41,19 @@ public class AbstractConnectionManager {
|
|||
|
||||
public static InputStream upgrade(DownloadableFile file, InputStream is) throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, InvalidKeyException, NoSuchPaddingException, NoSuchProviderException {
|
||||
if (file.getKey() != null && file.getIv() != null) {
|
||||
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(file.getKey(), KEYTYPE);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(file.getIv());
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
|
||||
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
||||
cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
|
||||
return new CipherInputStream(is, cipher);
|
||||
} else {
|
||||
return is;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static OutputStream createAppendedOutputStream(DownloadableFile file) {
|
||||
return createOutputStream(file, false, true);
|
||||
}
|
||||
|
||||
public static OutputStream createOutputStream(DownloadableFile file) {
|
||||
return createOutputStream(file, false);
|
||||
}
|
||||
|
||||
public static OutputStream createOutputStream(DownloadableFile file, boolean gcm) {
|
||||
return createOutputStream(file, gcm, false);
|
||||
}
|
||||
|
||||
private static OutputStream createOutputStream(DownloadableFile file, boolean gcm, boolean append) {
|
||||
public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) {
|
||||
FileOutputStream os;
|
||||
try {
|
||||
os = new FileOutputStream(file, append);
|
||||
if (file.getKey() == null) {
|
||||
if (file.getKey() == null || !decrypt) {
|
||||
return os;
|
||||
}
|
||||
} catch (FileNotFoundException e) {
|
||||
|
@ -76,18 +61,9 @@ public class AbstractConnectionManager {
|
|||
return null;
|
||||
}
|
||||
try {
|
||||
if (gcm) {
|
||||
final Cipher cipher = Compatibility.twentyEight() ? Cipher.getInstance(CIPHERMODE) : Cipher.getInstance(CIPHERMODE, PROVIDER);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(file.getKey(), KEYTYPE);
|
||||
IvParameterSpec ivSpec = new IvParameterSpec(file.getIv());
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
|
||||
return new CipherOutputStream(os, cipher);
|
||||
} else {
|
||||
IvParameterSpec ips = new IvParameterSpec(file.getIv());
|
||||
final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), KEYTYPE), ips);
|
||||
return new CipherOutputStream(os, cipher);
|
||||
}
|
||||
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
|
||||
cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
|
||||
return new CipherOutputStream(os, cipher);
|
||||
} catch (Exception e) {
|
||||
Log.d(Config.LOGTAG, "unable to create cipher output stream", e);
|
||||
return null;
|
||||
|
@ -138,4 +114,23 @@ public class AbstractConnectionManager {
|
|||
PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
|
||||
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
|
||||
}
|
||||
|
||||
public static class Extension {
|
||||
public final String main;
|
||||
public final String secondary;
|
||||
|
||||
private Extension(String main, String secondary) {
|
||||
this.main = main;
|
||||
this.secondary = secondary;
|
||||
}
|
||||
|
||||
public static Extension of(String path) {
|
||||
final int pos = path.lastIndexOf('/');
|
||||
final String filename = path.substring(pos + 1).toLowerCase();
|
||||
final String[] parts = filename.split("\\.");
|
||||
final String main = parts.length >= 2 ? parts[parts.length - 1] : null;
|
||||
final String secondary = parts.length >= 3 ? parts[parts.length - 2] : null;
|
||||
return new Extension(main, secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
package de.pixart.messenger.services;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.text.emoji.EmojiCompat;
|
||||
import android.util.Log;
|
||||
|
||||
import de.pixart.messenger.Config;
|
||||
|
||||
public abstract class AbstractEmojiService {
|
||||
|
||||
protected final Context context;
|
||||
|
||||
public AbstractEmojiService(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
protected abstract EmojiCompat.Config buildConfig();
|
||||
|
||||
public void init(boolean useBundledEmoji) {
|
||||
Log.d(Config.LOGTAG, "Emojis: use integrated lib " + useBundledEmoji);
|
||||
final EmojiCompat.Config config = buildConfig();
|
||||
//On recent Androids we assume to have the latest emojis
|
||||
//there are some annoying bugs with emoji compat that make it a safer choice not to use it when possible
|
||||
// a) when using the ondemand emoji font (play store) flags don’t work
|
||||
// b) the text preview has annoying glitches when the cut of text contains emojis (the emoji will be half visible)
|
||||
config.setReplaceAll(useBundledEmoji && Build.VERSION.SDK_INT < Build.VERSION_CODES.O);
|
||||
|
||||
EmojiCompat.init(config);
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ public abstract class AbstractQuickConversationsService {
|
|||
public abstract void considerSync();
|
||||
|
||||
public static boolean isQuicksy() {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isConversations() {
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
package de.pixart.messenger.services;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import net.ypresto.androidtranscoder.MediaTranscoder;
|
||||
import net.ypresto.androidtranscoder.format.MediaFormatStrategyPresets;
|
||||
|
||||
|
@ -178,6 +182,11 @@ public class AttachFileToConversationRunnable implements Runnable, MediaTranscod
|
|||
}
|
||||
}
|
||||
|
||||
public static String getVideoCompression(final Context context) {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
return preferences.getString("video_compression", context.getResources().getString(R.string.video_compression));
|
||||
}
|
||||
|
||||
public FileBackend getFileBackend() {
|
||||
return mXmppConnectionService.fileBackend;
|
||||
}
|
||||
|
|
|
@ -12,8 +12,7 @@ import android.media.AudioManager;
|
|||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.util.Base64;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImageButton;
|
||||
|
@ -21,6 +20,10 @@ import android.widget.RelativeLayout;
|
|||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
@ -89,7 +92,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
|
|||
audioPlayer.setTag(message);
|
||||
if (init(ViewHolder.get(audioPlayer), message)) {
|
||||
this.audioPlayerLayouts.addWeakReferenceTo(audioPlayer);
|
||||
executor.execute(()-> this.stopRefresher(true));
|
||||
executor.execute(() -> this.stopRefresher(true));
|
||||
} else {
|
||||
this.audioPlayerLayouts.removeWeakReferenceTo(audioPlayer);
|
||||
}
|
||||
|
@ -105,7 +108,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
|
|||
}
|
||||
viewHolder.progress.setOnSeekBarChangeListener(this);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
ColorStateList color = ContextCompat.getColorStateList(messageAdapter.getContext(), viewHolder.darkBackground ? R.color.white70 : R.color.bubble);
|
||||
ColorStateList color = viewHolder.darkBackground ? ContextCompat.getColorStateList(messageAdapter.getContext(), R.color.white70) : viewHolder.isOrange ? ContextCompat.getColorStateList(messageAdapter.getContext(), R.color.darkorange) : ContextCompat.getColorStateList(messageAdapter.getContext(), R.color.darkblue);
|
||||
viewHolder.progress.setThumbTintList(color);
|
||||
viewHolder.progress.setProgressTintList(color);
|
||||
}
|
||||
|
@ -139,9 +142,9 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
|
|||
}
|
||||
|
||||
private void startStop(ImageButton playPause) {
|
||||
if (ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission(messageAdapter.getActivity(), Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
pendingOnClickView.push(new WeakReference<>(playPause));
|
||||
ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_PLAY_PAUSE);
|
||||
ActivityCompat.requestPermissions(messageAdapter.getActivity(), new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, ConversationsActivity.REQUEST_PLAY_PAUSE);
|
||||
return;
|
||||
}
|
||||
initializeProximityWakeLock(playPause.getContext());
|
||||
|
@ -343,8 +346,12 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
|
|||
return false;
|
||||
}
|
||||
final ViewHolder viewHolder = ViewHolder.get(audioPlayer);
|
||||
viewHolder.progress.setProgress(current * 100 / duration);
|
||||
viewHolder.runtime.setText(formatTime(current) + " / " + formatTime(duration));
|
||||
if (duration <= 0) {
|
||||
viewHolder.progress.setProgress(100);
|
||||
} else {
|
||||
viewHolder.progress.setProgress(current * 100 / duration);
|
||||
}
|
||||
viewHolder.runtime.setText(String.format("%s / %s", formatTime(current), formatTime(duration)));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -373,6 +380,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
|
|||
try {
|
||||
ViewHolder currentViewHolder = getCurrentViewHolder();
|
||||
if (currentViewHolder != null) {
|
||||
messageAdapter.getActivity().setVolumeControlStream(streamType);
|
||||
play(currentViewHolder, currentlyPlayingMessage, streamType == AudioManager.STREAM_VOICE_CALL, progress);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
|
@ -417,6 +425,7 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
|
|||
private SeekBar progress;
|
||||
private ImageButton playPause;
|
||||
private boolean darkBackground = false;
|
||||
private boolean isOrange = false;
|
||||
|
||||
public static ViewHolder get(RelativeLayout audioPlayer) {
|
||||
ViewHolder viewHolder = (ViewHolder) audioPlayer.getTag(R.id.TAG_AUDIO_PLAYER_VIEW_HOLDER);
|
||||
|
@ -430,8 +439,9 @@ public class AudioPlayer implements View.OnClickListener, MediaPlayer.OnCompleti
|
|||
return viewHolder;
|
||||
}
|
||||
|
||||
public void setDarkBackground(boolean darkBackground) {
|
||||
public void setTheme(boolean darkBackground, boolean isOrange) {
|
||||
this.darkBackground = darkBackground;
|
||||
this.isOrange = isOrange;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,14 +13,15 @@ import android.graphics.Typeface;
|
|||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.res.ResourcesCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
@ -38,7 +39,8 @@ import de.pixart.messenger.entities.Conversational;
|
|||
import de.pixart.messenger.entities.ListItem;
|
||||
import de.pixart.messenger.entities.Message;
|
||||
import de.pixart.messenger.entities.MucOptions;
|
||||
import de.pixart.messenger.http.services.MuclumbusService;
|
||||
import de.pixart.messenger.entities.RawBlockable;
|
||||
import de.pixart.messenger.entities.Room;
|
||||
import de.pixart.messenger.utils.UIHelper;
|
||||
import de.pixart.messenger.xmpp.OnAdvancedStreamFeaturesLoaded;
|
||||
import de.pixart.messenger.xmpp.XmppConnection;
|
||||
|
@ -58,6 +60,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
|
|||
private static final String PREFIX_CONVERSATION = "conversation";
|
||||
private static final String PREFIX_ACCOUNT = "account";
|
||||
private static final String PREFIX_GENERIC = "generic";
|
||||
private static final String CHANNEL_SYMBOL = "#";
|
||||
|
||||
final private ArrayList<Integer> sizes = new ArrayList<>();
|
||||
final private HashMap<String, Set<String>> conversationDependentKeys = new HashMap<>();
|
||||
|
@ -83,19 +86,19 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
|
|||
return get((ListItem) avatarable, size, cachedOnly);
|
||||
} else if (avatarable instanceof MucOptions.User) {
|
||||
return get((MucOptions.User) avatarable, size, cachedOnly);
|
||||
} else if (avatarable instanceof MuclumbusService.Room) {
|
||||
return get((MuclumbusService.Room) avatarable, size, cachedOnly);
|
||||
} else if (avatarable instanceof Room) {
|
||||
return get((Room) avatarable, size, cachedOnly);
|
||||
}
|
||||
throw new AssertionError("AvatarService does not know how to generate avatar from "+avatarable.getClass().getName());
|
||||
throw new AssertionError("AvatarService does not know how to generate avatar from " + avatarable.getClass().getName());
|
||||
}
|
||||
|
||||
private Bitmap get(final MuclumbusService.Room result, final int size, boolean cacheOnly) {
|
||||
private Bitmap get(final Room result, final int size, boolean cacheOnly) {
|
||||
final Jid room = result.getRoom();
|
||||
Conversation conversation = room != null ? mXmppConnectionService.findFirstMuc(room) : null;
|
||||
if (conversation != null) {
|
||||
return get(conversation, size, cacheOnly);
|
||||
}
|
||||
return get(result.getName(), room != null ? room.asBareJid().toEscapedString() : result.getName(), size, cacheOnly);
|
||||
return get(CHANNEL_SYMBOL, room != null ? room.asBareJid().toEscapedString() : result.getName(), size, cacheOnly);
|
||||
}
|
||||
|
||||
private Bitmap get(final Contact contact, final int size, boolean cachedOnly) {
|
||||
|
@ -275,7 +278,9 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
|
|||
}
|
||||
|
||||
public Bitmap get(ListItem item, int size, boolean cachedOnly) {
|
||||
if (item instanceof Contact) {
|
||||
if (item instanceof RawBlockable) {
|
||||
return get(item.getDisplayName(), item.getJid().toEscapedString(), size, cachedOnly);
|
||||
} else if (item instanceof Contact) {
|
||||
return get((Contact) item, size, cachedOnly);
|
||||
} else if (item instanceof Bookmark) {
|
||||
Bookmark bookmark = (Bookmark) item;
|
||||
|
@ -338,12 +343,16 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
|
|||
bitmap = mXmppConnectionService.getFileBackend().getAvatar(mucOptions.getAvatar(), size);
|
||||
|
||||
if (bitmap == null) {
|
||||
final List<MucOptions.User> users = mucOptions.getUsersRelevantForNameAndAvatar();
|
||||
if (users.size() == 0) {
|
||||
Conversation c = mucOptions.getConversation();
|
||||
bitmap = getImpl(c.getName().toString(), c.getJid().asBareJid().toString(), size);
|
||||
Conversation c = mucOptions.getConversation();
|
||||
if (mucOptions.isPrivateAndNonAnonymous()) {
|
||||
final List<MucOptions.User> users = mucOptions.getUsersRelevantForNameAndAvatar();
|
||||
if (users.size() == 0) {
|
||||
bitmap = getImpl(c.getName().toString(), c.getJid().asBareJid().toString(), size);
|
||||
} else {
|
||||
bitmap = getImpl(users, size);
|
||||
}
|
||||
} else {
|
||||
bitmap = getImpl(users, size);
|
||||
bitmap = getImpl(CHANNEL_SYMBOL, c.getJid().asBareJid().toString(), size);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -627,7 +636,7 @@ public class AvatarService implements OnAdvancedStreamFeaturesLoaded {
|
|||
|
||||
private static boolean drawTile(Canvas canvas, String name, String seed, int left, int top, int right, int bottom) {
|
||||
if (name != null) {
|
||||
final String letter = getFirstLetter(name);
|
||||
final String letter = name.equals(CHANNEL_SYMBOL) ? name : getFirstLetter(name);
|
||||
final int color = UIHelper.getColorForName(seed == null ? name : seed);
|
||||
drawTile(canvas, letter, color, left, top, right, bottom);
|
||||
return true;
|
||||
|
|