Merge branch 'development'

This commit is contained in:
Daniel Gultsch 2015-08-15 14:26:37 +02:00
commit 1f34fb742c
201 changed files with 6942 additions and 1248 deletions

View File

@ -1,5 +1,11 @@
###Changelog
####Version 1.6.0
* new multi-end-to-multi-end encryption method
* redesigned chat bubbles
* show unexpected encryption changes as red chat bubbles
* always notify in private/non-anonymous conferences
####Version 1.5.1
* fixed rare crashes
* improved otr support

View File

@ -39,24 +39,24 @@ support these extensions; therefore to get the most out of Conversations you
should consider either switching to an XMPP server that does or — even better —
run your own XMPP server for you and your friends. These XEP's are:
* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Will be used to transfer
* [XEP-0065: SOCKS5 Bytestreams](http://xmpp.org/extensions/xep-0065.html) (or mod_proxy65). Will be used to transfer
files if both parties are behind a firewall (NAT).
* XEP-0163: Personal Eventing Protocol for avatars
* XEP-0191: Blocking command lets you blacklist spammers or block contacts
* [XEP-0163: Personal Eventing Protocol](http://xmpp.org/extensions/xep-0163.html) for avatars
* [XEP-0191: Blocking command](http://xmpp.org/extensions/xep-0191.html) lets you blacklist spammers or block contacts
without removing them from your roster.
* XEP-0198: Stream Management allows XMPP to survive small network outages and
* [XEP-0198: Stream Management](http://xmpp.org/extensions/xep-0198.html) allows XMPP to survive small network outages and
changes of the underlying TCP connection.
* XEP-0280: Message Carbons which automatically syncs the messages you send to
* [XEP-0280: Message Carbons](http://xmpp.org/extensions/xep-0280.html) which automatically syncs the messages you send to
your desktop client and thus allows you to switch seamlessly from your mobile
client to your desktop client and back within one conversation.
* XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections
* XEP-0313: Message Archive Management synchronize message history with the
* [XEP-0237: Roster Versioning](http://xmpp.org/extensions/xep-0237.html) mainly to save bandwidth on poor mobile connections
* [XEP-0313: Message Archive Management](http://xmpp.org/extensions/xep-0313.html) synchronize message history with the
server. Catch up with messages that were sent while Conversations was
offline.
* XEP-0352: Client State Indication lets the server know whether or not
* [XEP-0352: Client State Indication](http://xmpp.org/extensions/xep-0352.html) lets the server know whether or not
Conversations is in the background. Allows the server to save bandwidth by
withholding unimportant packages.
* XEP-xxxx: HttpUpload allows you to share files in conferences and with offline
* [XEP-xxxx: HTTP File Upload](http://xmpp.org/extensions/inbox/http-upload.html) allows you to share files in conferences and with offline
contacts. Requires an [additional component](https://github.com/siacs/HttpUploadComponent)
on your server.
@ -81,6 +81,7 @@ run your own XMPP server for you and your friends. These XEP's are:
#### Logo
* [Ilia Rostovtsev](https://github.com/qooob) (Progress)
* [Diego Turtulici](http://efesto.eigenlab.org/~diesys) (Original)
* [fiaxh](https://github.com/fiaxh) (OMEMO)
#### Translations
Translations are managed on [Transifex](https://www.transifex.com/projects/p/conversations/)

View File

@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="md_switch_thumb_disable_centered_square.svg"
viewBox="0 0 120 120"
height="120"
width="120"
inkscape:version="0.91 r13725"
version="1.1"
id="svg2">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6">
<linearGradient
inkscape:collect="always"
id="linearGradient4222">
<stop
style="stop-color:#000000;stop-opacity:1"
offset="0"
id="stop4224" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4226" />
</linearGradient>
<linearGradient
id="linearGradient4179"
osb:paint="gradient">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4181" />
<stop
style="stop-color:#ffffff;stop-opacity:0.25454545"
offset="1"
id="stop4183" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4222"
id="linearGradient4228"
x1="159.38722"
y1="19.802504"
x2="212.27522"
y2="19.802504"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-260.32215,163.27594)" />
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4230"
x="-0.012"
width="1.024"
y="-0.012"
height="1.024">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.25916904"
id="feGaussianBlur4232" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4371"
x="-0.23999999"
width="1.48"
y="-0.23999999"
height="1.48">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="5.2888"
id="feGaussianBlur4373" />
</filter>
</defs>
<sodipodi:namedview
inkscape:current-layer="layer2"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="1400"
inkscape:cy="61.379767"
inkscape:cx="10.572032"
inkscape:zoom="3.8530612"
showgrid="false"
id="namedview4"
inkscape:window-height="1024"
inkscape:window-width="1680"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="PNG"
style="display:none"
sodipodi:insensitive="true"
transform="translate(0,-2.5)" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="SVG"
style="display:inline"
transform="translate(0,-2.5)">
<g
id="g6404">
<circle
style="opacity:1;fill:#000404;fill-opacity:0.45531915;stroke:none;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4371)"
id="circle4234"
cx="59.999996"
cy="66.499878"
r="26.444" />
<g
transform="translate(3.3103058e-6,0.33229253)"
id="g4148">
<circle
style="opacity:1;fill:#bdbdbd;fill-opacity:1;stroke:#bdbdbd;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4218"
cx="59.999996"
cy="62.167587"
r="25.916904" />
<circle
r="25.916904"
cy="183.07845"
cx="-74.490921"
id="circle4220"
style="opacity:0.3;fill:none;fill-opacity:1;stroke:url(#linearGradient4228);stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4230)"
transform="matrix(0,-1,1,0,-123.07845,-12.323334)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,153 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="md_switch_thumb_off_normal_centered.svg"
viewBox="0 0 120 120"
height="120"
width="120"
inkscape:version="0.91 r13725"
version="1.1"
id="svg2">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6">
<linearGradient
inkscape:collect="always"
id="linearGradient4222">
<stop
style="stop-color:#000000;stop-opacity:1"
offset="0"
id="stop4224" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4226" />
</linearGradient>
<linearGradient
id="linearGradient4179"
osb:paint="gradient">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4181" />
<stop
style="stop-color:#ffffff;stop-opacity:0.25454545"
offset="1"
id="stop4183" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4222"
id="linearGradient4228"
x1="159.38722"
y1="19.802504"
x2="212.27522"
y2="19.802504"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-260.32215,163.27594)" />
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4230"
x="-0.012"
width="1.024"
y="-0.012"
height="1.024">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.25916904"
id="feGaussianBlur4232" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4371"
x="-0.23999999"
width="1.48"
y="-0.23999999"
height="1.48">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="5.2888"
id="feGaussianBlur4373" />
</filter>
</defs>
<sodipodi:namedview
inkscape:current-layer="layer2"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="1400"
inkscape:cy="61.379767"
inkscape:cx="10.052965"
inkscape:zoom="3.8530612"
showgrid="false"
id="namedview4"
inkscape:window-height="1024"
inkscape:window-width="1680"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="PNG"
style="display:none"
sodipodi:insensitive="true"
transform="translate(0,-2.5)" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="SVG"
style="display:inline"
transform="translate(0,-2.5)">
<circle
r="26.444"
cy="66.5"
cx="59.999996"
id="circle4234"
style="opacity:1;fill:#000404;fill-opacity:0.45531915;stroke:none;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4371)" />
<g
id="g6390"
transform="translate(3.3103058e-6,-0.91758577)">
<circle
r="25.916904"
cy="63.417587"
cx="59.999996"
id="path4218"
style="opacity:1;fill:#fafafa;fill-opacity:1;stroke:#fafafa;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
transform="matrix(0,-1,1,0,-123.07845,-11.073334)"
style="opacity:0.3;fill:none;fill-opacity:1;stroke:url(#linearGradient4228);stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4230)"
id="circle4220"
cx="-74.490921"
cy="183.07845"
r="25.916904" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="md_switch_thumb_off_pressed_centered.svg"
viewBox="0 0 120 120"
height="120"
width="120"
inkscape:version="0.91 r13725"
version="1.1"
id="svg2">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6">
<linearGradient
inkscape:collect="always"
id="linearGradient4222">
<stop
style="stop-color:#000000;stop-opacity:1"
offset="0"
id="stop4224" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4226" />
</linearGradient>
<linearGradient
id="linearGradient4179"
osb:paint="gradient">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4181" />
<stop
style="stop-color:#ffffff;stop-opacity:0.25454545"
offset="1"
id="stop4183" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4222"
id="linearGradient4228"
x1="159.38722"
y1="19.802504"
x2="212.27522"
y2="19.802504"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-260.32215,163.27594)" />
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4230"
x="-0.012"
width="1.024"
y="-0.012"
height="1.024">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.25916904"
id="feGaussianBlur4232" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4371"
x="-0.23999999"
width="1.48"
y="-0.23999999"
height="1.48">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="5.2888"
id="feGaussianBlur4373" />
</filter>
</defs>
<sodipodi:namedview
inkscape:current-layer="layer2"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="1400"
inkscape:cy="61.379767"
inkscape:cx="10.572032"
inkscape:zoom="3.8530612"
showgrid="false"
id="namedview4"
inkscape:window-height="1024"
inkscape:window-width="1680"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="PNG"
style="display:none"
sodipodi:insensitive="true"
transform="translate(0,-2.5)" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="SVG"
style="display:inline"
transform="translate(0,-2.5)">
<circle
style="opacity:1;fill:#313131;fill-opacity:0.10196078;fill-rule:nonzero;stroke:none;stroke-width:1.00100005;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.10196078"
id="path4819"
cx="60"
cy="62.5"
r="60" />
<circle
r="26.444"
cy="66.5"
cx="59.999996"
id="circle4234"
style="opacity:1;fill:#000404;fill-opacity:0.45531915;stroke:none;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4371)" />
<g
id="g6417"
transform="translate(3.3103058e-6,-0.91758577)">
<circle
r="25.916904"
cy="63.417587"
cx="59.999996"
id="path4218"
style="opacity:1;fill:#fafafa;fill-opacity:1;stroke:#fafafa;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
transform="matrix(0,-1,1,0,-123.07845,-11.073334)"
style="opacity:0.3;fill:none;fill-opacity:1;stroke:url(#linearGradient4228);stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4230)"
id="circle4220"
cx="-74.490921"
cy="183.07845"
r="25.916904" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -0,0 +1,146 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="md_switch_thumb_on_normal_centered_square.svg"
viewBox="0 0 120 120"
height="120"
width="120"
inkscape:version="0.91 r13725"
version="1.1"
id="svg2">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6">
<linearGradient
inkscape:collect="always"
id="linearGradient4222">
<stop
style="stop-color:#000000;stop-opacity:1"
offset="0"
id="stop4224" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4226" />
</linearGradient>
<linearGradient
id="linearGradient4179"
osb:paint="gradient">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4181" />
<stop
style="stop-color:#ffffff;stop-opacity:0.25454545"
offset="1"
id="stop4183" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4222"
id="linearGradient4228"
x1="159.38722"
y1="19.802504"
x2="212.27522"
y2="19.802504"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-260.32215,163.27594)" />
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4230"
x="-0.012"
width="1.024"
y="-0.012"
height="1.024">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.25916904"
id="feGaussianBlur4232" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4371"
x="-0.23999999"
width="1.48"
y="-0.23999999"
height="1.48">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="5.2888"
id="feGaussianBlur4373" />
</filter>
</defs>
<sodipodi:namedview
inkscape:current-layer="layer2"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="1400"
inkscape:cy="61.379767"
inkscape:cx="-14.397519"
inkscape:zoom="3.8530612"
showgrid="false"
id="namedview4"
inkscape:window-height="1024"
inkscape:window-width="1680"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="SVG"
style="display:inline"
transform="translate(0,-2.5)">
<circle
r="26.444"
cy="66.499878"
cx="59.999996"
id="circle4234"
style="opacity:1;fill:#000404;fill-opacity:0.45531915;stroke:none;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4371)" />
<g
id="g6440"
transform="translate(3.3103058e-6,0.33241423)">
<circle
r="25.916904"
cy="62.167587"
cx="59.999996"
id="path4218"
style="opacity:1;fill:#0091ea;fill-opacity:1;stroke:#0091ea;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
transform="matrix(0,-1,1,0,-123.07845,-12.323334)"
style="opacity:0.3;fill:none;fill-opacity:1;stroke:url(#linearGradient4228);stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4230)"
id="circle4220"
cx="-74.490921"
cy="183.07845"
r="25.916904" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,162 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
sodipodi:docname="md_switch_thumb_on_pressed_centered_square.svg"
viewBox="0 0 120 120"
height="120"
width="120"
inkscape:version="0.91 r13725"
version="1.1"
id="svg2">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6">
<linearGradient
inkscape:collect="always"
id="linearGradient4222">
<stop
style="stop-color:#000000;stop-opacity:1"
offset="0"
id="stop4224" />
<stop
style="stop-color:#ffffff;stop-opacity:1"
offset="1"
id="stop4226" />
</linearGradient>
<linearGradient
id="linearGradient4179"
osb:paint="gradient">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop4181" />
<stop
style="stop-color:#ffffff;stop-opacity:0.25454545"
offset="1"
id="stop4183" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4222"
id="linearGradient4228"
x1="159.38722"
y1="19.802504"
x2="212.27522"
y2="19.802504"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-260.32215,163.27594)" />
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4230"
x="-0.012"
width="1.024"
y="-0.012"
height="1.024">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="0.25916904"
id="feGaussianBlur4232" />
</filter>
<filter
inkscape:collect="always"
style="color-interpolation-filters:sRGB"
id="filter4371"
x="-0.23999999"
width="1.48"
y="-0.23999999"
height="1.48">
<feGaussianBlur
inkscape:collect="always"
stdDeviation="5.2888"
id="feGaussianBlur4373" />
</filter>
</defs>
<sodipodi:namedview
inkscape:current-layer="layer2"
inkscape:window-maximized="1"
inkscape:window-y="0"
inkscape:window-x="1400"
inkscape:cy="61.379767"
inkscape:cx="-46.31369"
inkscape:zoom="3.8530612"
showgrid="false"
id="namedview4"
inkscape:window-height="1024"
inkscape:window-width="1680"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="PNG"
style="display:none"
sodipodi:insensitive="true"
transform="translate(0,-2.5)" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="SVG"
style="display:inline"
transform="translate(0,-2.5)">
<circle
style="opacity:1;fill:#0093e8;fill-opacity:0.10196078;fill-rule:nonzero;stroke:none;stroke-width:1.00100005;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.10196078"
id="path4819"
cx="60"
cy="62.5"
r="60" />
<g
id="g4156">
<circle
style="opacity:1;fill:#000404;fill-opacity:0.45531915;stroke:none;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4371)"
id="circle4234"
cx="59.999996"
cy="66.5"
r="26.444" />
<g
transform="translate(3.3103058e-6,0.33241423)"
id="g4149">
<circle
style="opacity:1;fill:#0091ea;fill-opacity:1;stroke:#0091ea;stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4218"
cx="59.999996"
cy="62.167587"
r="25.916904" />
<circle
r="25.916904"
cy="183.07845"
cx="-74.490921"
id="circle4220"
style="opacity:0.3;fill:none;fill-opacity:1;stroke:url(#linearGradient4228);stroke-width:1.05419147;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4230)"
transform="matrix(0,-1,1,0,-123.07845,-12.323334)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

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

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

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

After

Width:  |  Height:  |  Size: 4.2 KiB

165
art/message_bubble_sent.svg Normal file
View File

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

After

Width:  |  Height:  |  Size: 4.2 KiB

273
art/omemo_logo.svg Normal file
View File

@ -0,0 +1,273 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg4196"
version="1.1"
inkscape:version="0.91 r13725"
width="2367.5596"
height="1451.5084"
viewBox="0 0 2367.5595 1451.5084"
sodipodi:docname="omemo_logo.svg">
<metadata
id="metadata4202">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4200">
<linearGradient
id="linearGradient4245"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop4247" />
</linearGradient>
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1600"
inkscape:window-height="836"
id="namedview4198"
showgrid="false"
inkscape:zoom="0.32"
inkscape:cx="1158.7782"
inkscape:cy="667.71025"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg4196"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 1160.235,302.29735 271.9745,-131.35135 186.9826,134.44197 24.7249,151.44038 -86.5373,135.98729 z"
id="path4267"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#f57c00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 598.8809,1125.9476 -43.05491,131.8557 -21.52745,94.8553 4.0364,47.0913 67.27328,6.7273 80.72795,-58.5277 43.72764,-78.7098 7.40006,-55.1641 -21.52745,-71.9824 z"
id="path4259"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 709.52231,1171.4517 C 480.05218,1174.9052 321.72113,1008.3849 269.81593,895.97589 206.11648,758.02449 215.35674,596.92706 303.94612,450.17116 390.00741,320.24292 538.03872,188.34494 665.64434,170.1992 c 86.87989,-10.63238 215.40898,15.76659 250.11793,24.23821 35.046,8.55388 138.10213,41.16536 192.58973,67.91907 53.5186,26.27793 164.698,69.05834 309.1218,196.39025 100.3317,88.4579 183.2875,109.97875 279.7545,106.68109 52.9405,-1.80973 148.8273,-10.56706 171.5302,-24.72865 679.9746,-424.15329 639.4516,799.03733 13.1124,405.39142 -158.3183,-74.1014 -440.1478,10.5521 -637.0436,91.78671 -223.8429,92.3524 -350.01628,130.7858 -535.30499,133.5744 z"
id="path4225"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssccssssscss"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 2121.4484,451.36293 c -26.791,-0.0103 -69.7877,2.87028 -101.1871,10.73905 -68.1167,46.199 -138.5457,83.35128 -167.446,144.67176 -12.1866,25.8575 -15.1986,221.06115 -3.3883,250.53885 22.0574,55.0538 36.5353,68.5186 75.8437,113.5484 490.8133,255.43581 586.5854,-519.34849 196.1777,-519.49806 z"
id="path4225-4"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093"
sodipodi:nodetypes="scsscs" />
<path
style="fill:#f57c00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1879.4205,872.05219 c -30.0884,-43.2017 -23.0447,-213.01732 -11.2518,-239.49258 19.553,-43.89704 110.0168,-119.19707 177.1545,-153.50421 62.2867,-31.14337 245.3285,107.06591 242.3844,259.61033 -2.4489,126.88796 -74.9751,256.91706 -216.1596,260.51446 -95.0727,-15.7629 -143.2721,-56.9801 -192.1275,-127.128 z"
id="path4313"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sscscs"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#f57c00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 712.41248,50.975873 130.5787,23.17966 80.35619,97.354527 11.5898,38.63275 -335.33229,-24.72496 56.4038,-112.807627 z"
id="path4317"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1414.8968,95.744433 c -119.2326,0.1221 -252.577,46.677797 -357.9883,141.492197 59.2267,85.339 179.6057,681.13776 68.8789,839.94337 91.9688,196.1395 767.4955,273.501 557.166,-210.17391 -15.7049,-36.1151 -49.7142,-108.75426 -41.832,-193.48626 8.4493,-90.8299 56.4409,-192.1808 64.2324,-223.9238 57.3257,-233.5482 -98.225,-354.048497 -290.457,-353.851597 z m -37.9434,48.607397 c 179.9257,-1.202 313.9232,108.10167 295.8852,273.14927 -49.0308,223.244 -65.6093,352.99519 9.7574,506.70029 0.9067,322.06951 -372.1528,246.99471 -531.1856,150.28521 136.0694,-390.78747 -67.0566,-814.79857 -78.5644,-831.62107 107.9381,-67.831 213.0862,-97.9056 304.1074,-98.5137 z"
id="path4227-8"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093"
sodipodi:nodetypes="sccsssssccccs" />
<path
inkscape:connector-curvature="0"
style="fill:#f57c00;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1371.9686,142.84932 c -91.0213,0.60808 -196.1693,30.68269 -304.1075,98.51367 11.5078,16.82249 214.6339,440.83547 78.5645,831.62311 159.0328,96.7094 532.0903,171.7842 531.1836,-150.28521 -75.3667,-153.7051 -47.9691,-295.82084 1.0617,-519.06483 19.5833,-183.59134 -126.7767,-261.98875 -306.7023,-260.78674 z m 42.0957,76.75039 c 158.8265,-0.80887 251.0755,161.9003 140.5517,325.36606 -113.709,-40.69316 -178.0341,-143.3305 -350.0787,-233.47358 73.9173,-58.593 149.3003,-91.58576 209.527,-91.89248 z"
id="path4229-6"
sodipodi:nodetypes="sccccssccs"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1354.944,727.52278 -6.0809,177.36791 128.2608,-32.6939 7.361,-132.81901 c 65.526,-55.1437 -11.1658,-135.6742 -75.9144,-147.0284 -93.1144,-16.3282 -143.1451,90.3398 -53.6265,135.1734 z"
id="path4233"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccsc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 598.8809,1127.2931 c -14.1274,92.1644 -82.99521,244.9415 -51.12771,263.7113 36.46239,21.4761 172.66811,-90.819 192.40161,-197.7835 18.83652,133.4254 -129.0419,247.1826 -195.76526,219.9837 -38.73013,-15.7879 4.93336,-176.7045 54.49136,-285.9115 z"
id="path4257"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscsc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#f57c00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 713.64035,1156.0811 23.95231,78.8109 72.62957,139.0779 118.98884,69.5389 -1.5453,-78.0381 -40.9507,-101.9905 -65.67567,-100.4452 -27.04293,-32.4515 z"
id="path4261"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 719.04894,1169.2163 c 9.27185,21.6343 50.66928,211.7208 189.30053,231.7965 30.3325,4.3925 -14.6805,-140.6232 -105.85379,-251.8855 102.24809,93.7488 161.32989,298.1418 122.07959,299.7901 C 810.58,1453.7045 732.44164,1267.601 719.04894,1169.2163 Z"
id="path4255"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cscsc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#f57c00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 234.13659,657.90289 -48.22924,-33.49254 -68.9946,-29.47343 -68.324762,2.00956 -31.48299,135.97969 54.2579,195.59632 92.028162,42.154 87.08057,10.8767 79.91784,5.717 49.77454,-1.1406 z"
id="path4265"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093"
sodipodi:nodetypes="ccccccccccc" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.81825721px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 362.2228,973.50299 c -87.01468,18.7244 -206.31388,2.7914 -260.29527,-66.2183 C 40.854878,829.20969 44.412488,641.34522 72.212698,611.40084 98.152348,583.46053 206.19233,642.42569 258.48372,672.39141 226.33414,633.9643 97.758248,551.92129 22.266478,615.19423 c -39.234376,32.88402 -22.2634293,269.25766 24.02476,303.47066 82.593032,61.047 269.567992,98.25131 315.931562,54.8381 z"
id="path4263"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csscssc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
inkscape:connector-curvature="0"
style="fill:#f57c00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 705.09299,169.2159 c -15.02488,0.0627 -29.55297,0.81546 -43.06769,2.46948 -127.0389,18.06512 -274.41191,149.37803 -360.09102,278.72918 -88.19593,146.10416 -97.39583,306.48603 -33.97922,443.82493 51.67469,111.90981 209.30324,277.68941 437.75427,274.25121 103.34093,-1.5552 188.21293,-14.2523 282.05624,-41.2806 l 53.34803,-135.65461 7.6922,-153.845 -32.3069,-87.691 -68.46233,-87.69283 -22.3067,-99.22916 30.769,-200.76658 -28.3598,-161.94745 c -6.6814,-1.88509 -12.5089,-3.44563 -17.1054,-4.56753 -29.15558,-7.11615 -124.80661,-26.93763 -205.94068,-26.60004 z"
id="path4225-42-9"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 447.07437,279.1177 -67.22098,67.22099 -37.8601,46.3593 180.02862,4.63593 96.58187,45.58664 70.31161,81.90143 47.13196,130.5787 -4.63593,166.8935 -88.85533,154.531 -78.03816,55.63111 16.99841,16.2258 81.12878,2.318 72.62957,-32.4515 L 807.90426,918.10339 837.26515,722.62168 810.99488,518.64075 734.50204,415.87764 630.96627,335.52152 535.9297,298.43408 Z"
id="path4247"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 704.34544,1170.0033 C 474.87531,1173.4568 316.54426,1006.9366 264.63906,894.52759 200.93961,756.57613 210.17987,595.47873 298.76925,448.72283 384.83054,318.7946 532.86185,186.89663 660.46747,168.75089 c 86.87989,-10.63238 215.40898,15.76659 250.1179,24.23821 35.046,8.55388 138.10213,41.16536 192.58973,67.91907 53.5186,26.27792 164.698,69.05836 309.1218,196.39026 100.3317,88.4579 183.2875,109.9787 279.7545,106.6811 52.9405,-1.8098 148.8273,-10.5671 171.5302,-24.7287 679.9746,-424.15326 639.4516,799.03727 13.1124,405.39146 -158.3183,-74.1014 -440.1478,10.5521 -637.0436,91.78671 -223.8429,92.3524 -350.0163,130.7857 -535.30496,133.5743 z"
id="path4225-42"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssccssssscss"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 600.77485,188.39742 c 45.59931,-168.111187 93.61702,-207.521997 165.6011,-175.903587 28.11465,12.34913 88.59168,36.45928 110.93127,66.5789 46.81515,63.119057 81.36115,162.974077 99.35615,284.156557 -8.7416,75.03201 -41.5452,164.02089 -27.3175,238.20842 17.5559,91.54126 116.68213,142.15421 125.66043,234.93028 9.4985,98.1511 -22.9467,217.44721 -86.32323,293.93611 36.78763,-80.4955 64.77883,-202.86651 55.72773,-281.91641 -15.7564,-137.61237 -102.80503,-141.89728 -115.82623,-244.76458 -9.3046,-73.506 20.1158,-155.47823 24.0394,-229.46683 3.7424,-70.5705 -32.2949,-195.09979 -74.30353,-233.83762 C 781.36459,50.911903 652.78479,43.071673 600.77485,188.39742 Z"
id="path4405"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csscsscssssc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#f57c00;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 978.84877,929.24739 143.14363,-41.5225 87.4159,-74.3036 5.4635,-186.85146 -73.2108,-30.5956 -65.562,22.9467 -77.58163,46.986 -87.416,87.416 z"
id="path4289"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 913.12497,751.04553 c 45.1649,-86.3232 245.53153,-195.3877 312.51203,-177.0172 29.651,8.1322 84.3992,143.4773 -29.5028,270.98936 -50.127,56.1165 -219.63263,88.5086 -219.63263,88.5086 l 2.1854,-6.5562 c 0,0 154.41873,-31.7084 192.31513,-91.7868 38.4759,-60.9969 52.9259,-177.98746 0,-216.35436 -79.3518,-57.5234 -257.87713,132.2166 -257.87713,132.2166 z"
id="path4287"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cssccssc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 330.40347,402.7425 c 114.35294,-10.55962 249.75787,-2.93669 319.87917,81.90143 79.96514,96.74795 96.08396,242.01494 62.58506,351.55806 -23.16188,75.7405 -98.38474,154.531 -163.80286,199.34501 60.19701,36.3696 76.03151,31.5859 158.39427,3.8632 C 761.64992,1021.17 829.49446,914.53909 837.26515,833.88399 848.77388,714.43039 855.97093,574.91586 790.90585,472.28145 719.85004,360.19719 579.71348,287.1018 454.02827,276.79974 l -13.13513,9.27185 c 166.63592,15.4531 280.2303,99.1189 342.28616,214.02545 65.19894,120.72647 36.96723,291.05045 30.9062,330.69635 -11.59484,75.8433 -39.28607,162.0595 -121.30683,197.02701 -32.03238,13.6562 -80.61368,27.043 -116.67091,8.4993 C 636.37485,988.41499 708.98856,931.86239 729.09345,855.51829 762.38235,729.11061 744.53737,534.45916 642.55609,446.01118 573.9429,386.50322 397.62445,372.60895 347.40188,381.10816 Z"
id="path4245"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csscsssccssscsscc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="opacity:1;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path4249"
sodipodi:type="arc"
sodipodi:cx="592.71979"
sodipodi:cy="560.36414"
sodipodi:rx="41.337044"
sodipodi:ry="48.677265"
sodipodi:start="0"
sodipodi:end="6.2714218"
sodipodi:open="true"
d="m 634.05683,560.36414 a 41.337044,48.677265 0 0 1 -41.21548,48.67705 41.337044,48.677265 0 0 1 -41.45789,-48.39075 41.337044,48.677265 0 0 1 40.97163,-48.96167 41.337044,48.677265 0 0 1 41.69888,48.10276"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:15;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 329.82495,822.87809 c 50.9573,98.8977 80.17049,31.9344 81.80769,19.0432 2.98204,-23.4803 -26.03926,-8.0283 -44.87764,-12.8177 -24.76611,-6.2965 -49.64587,-30.9043 -36.93005,-6.2255 z"
id="path4253"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ssss"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 349.24366,1015.5476 c 24.85373,22.5357 29.23211,28.8458 48.29094,41.7233 13.9627,-4.7761 21.9738,-0.484 43.60813,-17.9975 -43.655,-2.9618 -58.6749,-15.7418 -91.89907,-23.7258 z"
id="path4339"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#000000;fill-opacity:0.11764706;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 1840.2193,917.45789 c -144.4722,-64.5024 -401.6544,9.1852 -581.3301,79.8965 C 1054.6229,1077.7432 939.48447,1111.1985 770.40085,1113.6259 560.99961,1116.632 416.51658,971.68219 369.15085,873.83489 311.02239,753.75418 319.45382,613.52678 400.29538,485.78208 478.83,372.68518 613.91429,257.87398 730.35984,242.07898 c 12.38775,-1.4461 25.70459,-2.1055 39.47656,-2.1601 74.36866,-0.2952 162.04327,17.0358 188.76757,23.2578 31.981,7.4458 126.02373,35.8332 175.74613,59.1211 48.8379,22.8738 150.2931,60.1123 282.0859,170.9492 91.5569,76.9988 167.2589,95.7317 255.2891,92.8613 48.3104,-1.5753 135.8099,-9.1984 156.5274,-21.5254 l 39.4824,-26.4785 c -22.7029,14.1616 -118.5888,22.9187 -171.5293,24.7285 -96.467,3.2976 -179.4222,-18.2237 -279.7539,-106.6816 -144.4238,-127.3319 -255.6045,-170.1108 -309.1231,-196.3887 -54.4876,-26.7537 -157.54383,-59.3661 -192.58983,-67.9199 -29.28571,-7.148 -125.36331,-27.0578 -206.8594,-26.7188 -15.09187,0.063 -29.68283,0.8192 -43.25782,2.4805 -127.60562,18.1458 -275.63793,150.0445 -361.69921,279.9727 -88.58938,146.7559 -97.82836,307.8532 -34.12891,445.80461 51.9052,112.40901 210.23495,278.92821 439.70508,275.47471 185.28865,-2.7886 311.46379,-41.2219 535.30669,-133.5743 196.8958,-81.23461 478.7247,-165.88851 637.043,-91.78711 z"
id="path4225-42-3"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csssccsssssccsssssccssscc"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
<path
style="fill:#000000;fill-opacity:0.11764706;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 707.63885,164.4148 c -15.0918,0.063 -29.6848,0.8211 -43.2597,2.4824 -127.6056,18.1458 -275.6361,150.0425 -361.6973,279.9707 -88.5894,146.7559 -97.8304,307.8533 -34.1309,445.80469 51.9052,112.40901 210.237,278.93011 439.7071,275.47661 185.2887,-2.7886 311.46185,-41.2218 535.30475,-133.5742 195.3026,-80.57741 474.1694,-164.51701 633.1757,-93.55281 -7.0564,-4.019 -14.1914,-8.2481 -21.4101,-12.7011 -151.9427,-69.8031 -422.4225,9.9405 -611.3887,86.46281 -214.8282,86.9953 -335.92133,123.1994 -513.74805,125.8262 -220.2288,3.2531 -372.1832,-153.60771 -421.9981,-259.49611 -61.1341,-129.94919 -52.2658,-281.70249 32.7559,-419.94529 82.5954,-122.3913 224.6641,-246.6373 347.1308,-263.7305 83.381,-10.0156 206.734,14.8519 240.04502,22.8321 33.6347,8.0576 132.54073,38.7767 184.83393,63.9785 51.3632,24.7535 158.0664,65.0525 296.6738,184.998 96.2911,83.3266 175.9061,103.5985 268.4883,100.4922 50.8085,-1.7048 142.8325,-9.9528 164.6211,-23.2929 0.3844,-0.2354 0.7646,-0.4611 1.1485,-0.6954 -38.3016,9.244 -106.3509,14.9537 -147.9278,16.375 -96.467,3.2976 -179.4222,-18.2237 -279.7539,-106.6816 C 1271.7855,328.1122 1160.6067,285.3314 1107.0881,259.0535 1052.6005,232.2998 949.54427,199.6894 914.49827,191.1355 885.21265,183.9876 789.13495,164.0757 707.63885,164.4148 Z"
id="path4421"
inkscape:connector-curvature="0"
inkscape:export-xdpi="15.191093"
inkscape:export-ydpi="15.191093" />
</svg>

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,11 +1,15 @@
#!/bin/env ruby
resolutions={
'mdpi'=> 1,
require 'xml'
resolutions = {
'mdpi' => 1,
'hdpi' => 1.5,
'xhdpi' => 2,
'xxhdpi' => 3,
'xxxhdpi' => 4,
}
images = {
'conversations_baloon.svg' => ['ic_launcher', 48],
'conversations_mono.svg' => ['ic_notification', 24],
@ -33,14 +37,92 @@ images = {
'ic_send_picture_online.svg' => ['ic_send_picture_online', 36],
'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36],
'ic_send_picture_away.svg' => ['ic_send_picture_away', 36],
'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36]
'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36],
'md_switch_thumb_disable.svg' => ['switch_thumb_disable', 48],
'md_switch_thumb_off_normal.svg' => ['switch_thumb_off_normal', 48],
'md_switch_thumb_off_pressed.svg' => ['switch_thumb_off_pressed', 48],
'md_switch_thumb_on_normal.svg' => ['switch_thumb_on_normal', 48],
'md_switch_thumb_on_pressed.svg' => ['switch_thumb_on_pressed', 48],
'message_bubble_received.svg' => ['message_bubble_received.9', 0],
'message_bubble_received_warning.svg' => ['message_bubble_received_warning.9', 0],
'message_bubble_sent.svg' => ['message_bubble_sent.9', 0],
}
images.each do |source, result|
resolutions.each do |name, factor|
size = factor * result[1]
path = "../src/main/res/drawable-#{name}/#{result[0]}.png"
cmd = "inkscape -e #{path} -C -h #{size} -w #{size} #{source}"
# Executable paths for Mac OSX
# "/Applications/Inkscape.app/Contents/Resources/bin/inkscape"
inkscape = "inkscape"
imagemagick = "convert"
def execute_cmd(cmd)
puts cmd
system cmd
end
images.each do |source_filename, settings|
svg_content = File.read(source_filename)
svg = XML::Document.string(svg_content)
base_width = svg.root["width"].to_i
base_height = svg.root["height"].to_i
guides = svg.find(".//sodipodi:guide")
resolutions.each do |resolution, factor|
output_filename, base_size = settings
if base_size > 0
width = factor * base_size
height = factor * base_size
else
width = factor * base_width
height = factor * base_height
end
path = "../src/main/res/drawable-#{resolution}/#{output_filename}.png"
execute_cmd "#{inkscape} -f #{source_filename} -z -C -w #{width} -h #{height} -e #{path}"
top = []
right = []
bottom = []
left = []
guides.each do |guide|
orientation = guide["orientation"]
x, y = guide["position"].split(",")
x, y = x.to_i, y.to_i
if orientation == "1,0" and y == base_height
top.push(x * factor)
end
if orientation == "0,1" and x == base_width
right.push((base_height - y) * factor)
end
if orientation == "1,0" and y == 0
bottom.push(x * factor)
end
if orientation == "0,1" and x == 0
left.push((base_height - y) * factor)
end
end
next if top.length != 2
next if right.length != 2
next if bottom.length != 2
next if left.length != 2
execute_cmd "#{imagemagick} -background none PNG32:#{path} -gravity center -extent #{width+2}x#{height+2} PNG32:#{path}"
draw_format = "-draw \"rectangle %d,%d %d,%d\""
top_line = draw_format % [top.min + 1, 0, top.max, 0]
right_line = draw_format % [width + 1, right.min + 1, width + 1, right.max]
bottom_line = draw_format % [bottom.min + 1, height + 1, bottom.max, height + 1]
left_line = draw_format % [0, left.min + 1, 0, left.max]
draws = "#{top_line} #{right_line} #{bottom_line} #{left_line}"
execute_cmd "#{imagemagick} -background none PNG32:#{path} -fill black -stroke none #{draws} PNG32:#{path}"
end
end

View File

@ -35,7 +35,10 @@ dependencies {
compile 'com.google.zxing:android-integration:3.1.0'
compile 'de.measite.minidns:minidns:0.1.3'
compile 'de.timroes.android:EnhancedListView:0.3.4'
compile 'me.leolin:ShortcutBadger:1.1.1@aar'
compile 'me.leolin:ShortcutBadger:1.1.3@aar'
compile 'com.kyleduo.switchbutton:library:1.2.8'
compile 'org.whispersystems:axolotl-android:1.3.4'
compile 'com.makeramen:roundedimageview:2.1.1'
}
android {
@ -45,8 +48,8 @@ android {
defaultConfig {
minSdkVersion 14
targetSdkVersion 21
versionCode 80
versionName "1.5.2"
versionCode 82
versionName "1.6.0-beta.2"
}
compileOptions {
@ -93,7 +96,7 @@ android {
}
lintOptions {
disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity'
disable 'ExtraTranslation', 'MissingTranslation', 'InvalidPackage', 'MissingQuantity', 'AppCompatResource'
}
subprojects {

View File

@ -130,6 +130,10 @@
<data android:mimeType="image/*" />
</intent-filter>
</activity>
<activity
android:name=".ui.TrustKeysActivity"
android:label="@string/trust_omemo_fingerprints"
android:windowSoftInputMode="stateAlwaysHidden"/>
<activity
android:name="de.duenndns.ssl.MemorizingActivity"
android:theme="@style/ConversationsTheme"

View File

@ -8,6 +8,11 @@ public final class Config {
public static final String LOGTAG = "conversations";
public static final String DOMAIN_LOCK = null; //only allow account creation for this domain
public static final boolean DISALLOW_REGISTRATION_IN_UI = false; //hide the register checkbox
public static final boolean HIDE_PGP_IN_UI = false; //some more consumer focused clients might want to disable OpenPGP
public static final int PING_MAX_INTERVAL = 300;
public static final int PING_MIN_INTERVAL = 30;
public static final int PING_TIMEOUT = 10;
@ -19,12 +24,15 @@ public final class Config {
public static final int AVATAR_SIZE = 192;
public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;
public static final int IMAGE_SIZE = 1920;
public static final Bitmap.CompressFormat IMAGE_FORMAT = Bitmap.CompressFormat.JPEG;
public static final int IMAGE_QUALITY = 75;
public static final int MESSAGE_MERGE_WINDOW = 20;
public static final int PAGE_SIZE = 50;
public static final int MAX_NUM_PAGES = 3;
public static final int PROGRESS_UI_UPDATE_INTERVAL = 750;
public static final int REFRESH_UI_INTERVAL = 500;
public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb
@ -34,6 +42,10 @@ public final class Config {
public static final boolean ENCRYPT_ON_HTTP_UPLOADED = false;
public static final boolean REPORT_WRONG_FILESIZE_IN_OTR_JINGLE = true;
public static final boolean SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON = false;
public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2;
public static final int MAM_MAX_MESSAGES = 500;

View File

@ -1,5 +1,20 @@
package eu.siacs.conversations.crypto;
import android.util.Log;
import net.java.otr4j.OtrEngineHost;
import net.java.otr4j.OtrException;
import net.java.otr4j.OtrPolicy;
import net.java.otr4j.OtrPolicyImpl;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
import net.java.otr4j.crypto.OtrCryptoException;
import net.java.otr4j.session.FragmenterInstructions;
import net.java.otr4j.session.InstanceTag;
import net.java.otr4j.session.SessionID;
import org.json.JSONException;
import org.json.JSONObject;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
@ -11,31 +26,15 @@ import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import org.json.JSONException;
import org.json.JSONObject;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xmpp.chatstate.ChatState;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import net.java.otr4j.OtrEngineHost;
import net.java.otr4j.OtrException;
import net.java.otr4j.OtrPolicy;
import net.java.otr4j.OtrPolicyImpl;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
import net.java.otr4j.crypto.OtrCryptoException;
import net.java.otr4j.session.InstanceTag;
import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.FragmenterInstructions;
public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost {
private Account account;

View File

@ -1,5 +1,13 @@
package eu.siacs.conversations.crypto;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
@ -9,10 +17,6 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
@ -22,9 +26,6 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
public class PgpEngine {
private OpenPgpApi api;

View File

@ -0,0 +1,713 @@
package eu.siacs.conversations.crypto.axolotl;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.SessionBuilder;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.KeyHelper;
import java.security.Security;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.SerialSingleThreadExecutor;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class AxolotlService {
public static final String PEP_PREFIX = "eu.siacs.conversations.axolotl";
public static final String PEP_DEVICE_LIST = PEP_PREFIX + ".devicelist";
public static final String PEP_BUNDLES = PEP_PREFIX + ".bundles";
public static final String LOGPREFIX = "AxolotlService";
public static final int NUM_KEYS_TO_PUBLISH = 100;
private final Account account;
private final XmppConnectionService mXmppConnectionService;
private final SQLiteAxolotlStore axolotlStore;
private final SessionMap sessions;
private final Map<Jid, Set<Integer>> deviceIds;
private final Map<String, XmppAxolotlMessage> messageCache;
private final FetchStatusMap fetchStatusMap;
private final SerialSingleThreadExecutor executor;
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(AxolotlAddress 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(AxolotlAddress 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(AxolotlAddress address) {
synchronized (MAP_LOCK) {
Map<Integer, T> devices = map.get(address.getName());
if (devices == null) {
return new HashMap<>();
}
return devices;
}
}
public boolean hasAny(AxolotlAddress 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);
}
private void putDevicesForJid(String bareJid, List<Integer> deviceIds, SQLiteAxolotlStore store) {
for (Integer deviceId : deviceIds) {
AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString());
String fingerprint = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey().getFingerprint().replaceAll("\\s", "");
this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, fingerprint));
}
}
private void fillMap(SQLiteAxolotlStore store) {
List<Integer> deviceIds = store.getSubDeviceSessions(account.getJid().toBareJid().toString());
putDevicesForJid(account.getJid().toBareJid().toString(), deviceIds, store);
for (Contact contact : account.getRoster().getContacts()) {
Jid bareJid = contact.getJid().toBareJid();
if (bareJid == null) {
continue; // FIXME: handle this?
}
String address = bareJid.toString();
deviceIds = store.getSubDeviceSessions(address);
putDevicesForJid(address, deviceIds, store);
}
}
@Override
public void put(AxolotlAddress address, XmppAxolotlSession value) {
super.put(address, value);
value.setNotFresh();
xmppConnectionService.syncRosterToDisk(account);
}
public void put(XmppAxolotlSession session) {
this.put(session.getRemoteAddress(), session);
}
}
private static enum FetchStatus {
PENDING,
SUCCESS,
ERROR
}
private static class FetchStatusMap extends AxolotlAddressMap<FetchStatus> {
}
public static String getLogprefix(Account account) {
return LOGPREFIX + " (" + account.getJid().toBareJid().toString() + "): ";
}
public AxolotlService(Account account, XmppConnectionService connectionService) {
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();
}
public IdentityKey getOwnPublicKey() {
return axolotlStore.getIdentityKeyPair().getPublicKey();
}
public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust) {
return axolotlStore.getContactKeysWithTrust(account.getJid().toBareJid().toString(), trust);
}
public Set<IdentityKey> getKeysWithTrust(XmppAxolotlSession.Trust trust, Contact contact) {
return axolotlStore.getContactKeysWithTrust(contact.getJid().toBareJid().toString(), trust);
}
public long getNumTrustedKeys(Contact contact) {
return axolotlStore.getContactNumTrustedKeys(contact.getJid().toBareJid().toString());
}
private AxolotlAddress getAddressForJid(Jid jid) {
return new AxolotlAddress(jid.toString(), 0);
}
private Set<XmppAxolotlSession> findOwnSessions() {
AxolotlAddress ownAddress = getAddressForJid(account.getJid().toBareJid());
Set<XmppAxolotlSession> ownDeviceSessions = new HashSet<>(this.sessions.getAll(ownAddress).values());
return ownDeviceSessions;
}
private Set<XmppAxolotlSession> findSessionsforContact(Contact contact) {
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
Set<XmppAxolotlSession> sessions = new HashSet<>(this.sessions.getAll(contactAddress).values());
return sessions;
}
private boolean hasAny(Contact contact) {
AxolotlAddress contactAddress = getAddressForJid(contact.getJid());
return sessions.hasAny(contactAddress);
}
public void regenerateKeys() {
axolotlStore.regenerate();
sessions.clear();
fetchStatusMap.clear();
publishBundlesIfNeeded();
publishOwnDeviceIdIfNeeded();
}
public int getOwnDeviceId() {
return axolotlStore.getLocalRegistrationId();
}
public Set<Integer> getOwnDeviceIds() {
return this.deviceIds.get(account.getJid().toBareJid());
}
private void setTrustOnSessions(final Jid jid, @NonNull final Set<Integer> deviceIds,
final XmppAxolotlSession.Trust from,
final XmppAxolotlSession.Trust to) {
for (Integer deviceId : deviceIds) {
AxolotlAddress address = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
XmppAxolotlSession session = sessions.get(address);
if (session != null && session.getFingerprint() != null
&& session.getTrust() == from) {
session.setTrust(to);
}
}
}
public void registerDevices(final Jid jid, @NonNull final Set<Integer> deviceIds) {
if (jid.toBareJid().equals(account.getJid().toBareJid())) {
if (deviceIds.contains(getOwnDeviceId())) {
deviceIds.remove(getOwnDeviceId());
}
for (Integer deviceId : deviceIds) {
AxolotlAddress ownDeviceAddress = new AxolotlAddress(jid.toBareJid().toString(), deviceId);
if (sessions.get(ownDeviceAddress) == null) {
buildSessionFromPEP(ownDeviceAddress);
}
}
}
Set<Integer> expiredDevices = new HashSet<>(axolotlStore.getSubDeviceSessions(jid.toBareJid().toString()));
expiredDevices.removeAll(deviceIds);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.TRUSTED,
XmppAxolotlSession.Trust.INACTIVE_TRUSTED);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNDECIDED,
XmppAxolotlSession.Trust.INACTIVE_UNDECIDED);
setTrustOnSessions(jid, expiredDevices, XmppAxolotlSession.Trust.UNTRUSTED,
XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED);
Set<Integer> newDevices = new HashSet<>(deviceIds);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_TRUSTED,
XmppAxolotlSession.Trust.TRUSTED);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNDECIDED,
XmppAxolotlSession.Trust.UNDECIDED);
setTrustOnSessions(jid, newDevices, XmppAxolotlSession.Trust.INACTIVE_UNTRUSTED,
XmppAxolotlSession.Trust.UNTRUSTED);
this.deviceIds.put(jid, deviceIds);
mXmppConnectionService.keyStatusUpdated();
publishOwnDeviceIdIfNeeded();
}
public void wipeOtherPepDevices() {
Set<Integer> deviceIds = new HashSet<>();
deviceIds.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Wiping all other devices from Pep:" + publish);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
// TODO: implement this!
}
});
}
public void purgeKey(IdentityKey identityKey) {
axolotlStore.setFingerprintTrust(identityKey.getFingerprint().replaceAll("\\s", ""), XmppAxolotlSession.Trust.COMPROMISED);
}
public void publishOwnDeviceIdIfNeeded() {
IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveDeviceIds(account.getJid().toBareJid());
mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
Element item = mXmppConnectionService.getIqParser().getItem(packet);
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
if (deviceIds == null) {
deviceIds = new HashSet<Integer>();
}
if (!deviceIds.contains(getOwnDeviceId())) {
deviceIds.add(getOwnDeviceId());
IqPacket publish = mXmppConnectionService.getIqGenerator().publishDeviceIds(deviceIds);
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Own device " + getOwnDeviceId() + " not in PEP devicelist. Publishing: " + publish);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
// TODO: implement this!
}
});
}
}
});
}
public void publishBundlesIfNeeded() {
IqPacket packet = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(account.getJid().toBareJid(), getOwnDeviceId());
mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
PreKeyBundle bundle = mXmppConnectionService.getIqParser().bundle(packet);
Map<Integer, ECPublicKey> keys = mXmppConnectionService.getIqParser().preKeyPublics(packet);
boolean flush = false;
if (bundle == null) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid bundle:" + packet);
bundle = new PreKeyBundle(-1, -1, -1, null, -1, null, null, null);
flush = true;
}
if (keys == null) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received invalid prekeys:" + packet);
}
try {
boolean changed = false;
// Validate IdentityKey
IdentityKeyPair identityKeyPair = axolotlStore.getIdentityKeyPair();
if (flush || !identityKeyPair.getPublicKey().equals(bundle.getIdentityKey())) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding own IdentityKey " + identityKeyPair.getPublicKey() + " to PEP.");
changed = true;
}
// Validate signedPreKeyRecord + ID
SignedPreKeyRecord signedPreKeyRecord;
int numSignedPreKeys = axolotlStore.loadSignedPreKeys().size();
try {
signedPreKeyRecord = axolotlStore.loadSignedPreKey(bundle.getSignedPreKeyId());
if (flush
|| !bundle.getSignedPreKey().equals(signedPreKeyRecord.getKeyPair().getPublicKey())
|| !Arrays.equals(bundle.getSignedPreKeySignature(), signedPreKeyRecord.getSignature())) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
changed = true;
}
} catch (InvalidKeyIdException e) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding new signedPreKey with ID " + (numSignedPreKeys + 1) + " to PEP.");
signedPreKeyRecord = KeyHelper.generateSignedPreKey(identityKeyPair, numSignedPreKeys + 1);
axolotlStore.storeSignedPreKey(signedPreKeyRecord.getId(), signedPreKeyRecord);
changed = true;
}
// Validate PreKeys
Set<PreKeyRecord> preKeyRecords = new HashSet<>();
if (keys != null) {
for (Integer id : keys.keySet()) {
try {
PreKeyRecord preKeyRecord = axolotlStore.loadPreKey(id);
if (preKeyRecord.getKeyPair().getPublicKey().equals(keys.get(id))) {
preKeyRecords.add(preKeyRecord);
}
} catch (InvalidKeyIdException ignored) {
}
}
}
int newKeys = NUM_KEYS_TO_PUBLISH - preKeyRecords.size();
if (newKeys > 0) {
List<PreKeyRecord> newRecords = KeyHelper.generatePreKeys(
axolotlStore.getCurrentPreKeyId() + 1, newKeys);
preKeyRecords.addAll(newRecords);
for (PreKeyRecord record : newRecords) {
axolotlStore.storePreKey(record.getId(), record);
}
changed = true;
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Adding " + newKeys + " new preKeys to PEP.");
}
if (changed) {
IqPacket publish = mXmppConnectionService.getIqGenerator().publishBundles(
signedPreKeyRecord, axolotlStore.getIdentityKeyPair().getPublicKey(),
preKeyRecords, getOwnDeviceId());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + ": Bundle " + getOwnDeviceId() + " in PEP not current. Publishing: " + publish);
mXmppConnectionService.sendIqPacket(account, publish, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
// TODO: implement this!
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Published bundle, got: " + packet);
}
});
}
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to publish bundle " + getOwnDeviceId() + ", reason: " + e.getMessage());
return;
}
}
});
}
public boolean isContactAxolotlCapable(Contact contact) {
Jid jid = contact.getJid().toBareJid();
AxolotlAddress address = new AxolotlAddress(jid.toString(), 0);
return sessions.hasAny(address) ||
(deviceIds.containsKey(jid) && !deviceIds.get(jid).isEmpty());
}
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
return axolotlStore.getFingerprintTrust(fingerprint);
}
public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
axolotlStore.setFingerprintTrust(fingerprint, trust);
}
private void buildSessionFromPEP(final AxolotlAddress address) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building new sesstion for " + address.getDeviceId());
try {
IqPacket bundlesPacket = mXmppConnectionService.getIqGenerator().retrieveBundlesForDevice(
Jid.fromString(address.getName()), address.getDeviceId());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Retrieving bundle: " + bundlesPacket);
mXmppConnectionService.sendIqPacket(account, bundlesPacket, new OnIqPacketReceived() {
private void finish() {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
if (!fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
&& !fetchStatusMap.getAll(address).containsValue(FetchStatus.PENDING)) {
mXmppConnectionService.keyStatusUpdated();
}
}
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Received preKey IQ packet, processing...");
final IqParser parser = mXmppConnectionService.getIqParser();
final List<PreKeyBundle> preKeyBundleList = parser.preKeys(packet);
final PreKeyBundle bundle = parser.bundle(packet);
if (preKeyBundleList.isEmpty() || bundle == null) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "preKey IQ packet invalid: " + packet);
fetchStatusMap.put(address, FetchStatus.ERROR);
finish();
return;
}
Random random = new Random();
final PreKeyBundle preKey = preKeyBundleList.get(random.nextInt(preKeyBundleList.size()));
if (preKey == null) {
//should never happen
fetchStatusMap.put(address, FetchStatus.ERROR);
finish();
return;
}
final PreKeyBundle preKeyBundle = new PreKeyBundle(0, address.getDeviceId(),
preKey.getPreKeyId(), preKey.getPreKey(),
bundle.getSignedPreKeyId(), bundle.getSignedPreKey(),
bundle.getSignedPreKeySignature(), bundle.getIdentityKey());
axolotlStore.saveIdentity(address.getName(), bundle.getIdentityKey());
try {
SessionBuilder builder = new SessionBuilder(axolotlStore, address);
builder.process(preKeyBundle);
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, bundle.getIdentityKey().getFingerprint().replaceAll("\\s", ""));
sessions.put(address, session);
fetchStatusMap.put(address, FetchStatus.SUCCESS);
} catch (UntrustedIdentityException | InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error building session for " + address + ": "
+ e.getClass().getName() + ", " + e.getMessage());
fetchStatusMap.put(address, FetchStatus.ERROR);
}
finish();
}
});
} catch (InvalidJidException e) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got address with invalid jid: " + address.getName());
}
}
public Set<AxolotlAddress> findDevicesWithoutSession(final Conversation conversation) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Finding devices without session for " + conversation.getContact().getJid().toBareJid());
Jid contactJid = conversation.getContact().getJid().toBareJid();
Set<AxolotlAddress> addresses = new HashSet<>();
if (deviceIds.get(contactJid) != null) {
for (Integer foreignId : this.deviceIds.get(contactJid)) {
AxolotlAddress address = new AxolotlAddress(contactJid.toString(), foreignId);
if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
sessions.put(address, session);
} else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + foreignId);
addresses.add(new AxolotlAddress(contactJid.toString(), foreignId));
}
}
}
} else {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Have no target devices in PEP!");
}
if (deviceIds.get(account.getJid().toBareJid()) != null) {
for (Integer ownId : this.deviceIds.get(account.getJid().toBareJid())) {
AxolotlAddress address = new AxolotlAddress(account.getJid().toBareJid().toString(), ownId);
if (sessions.get(address) == null) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
if (identityKey != null) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already have session for " + address.toString() + ", adding to cache...");
XmppAxolotlSession session = new XmppAxolotlSession(account, axolotlStore, address, identityKey.getFingerprint().replaceAll("\\s", ""));
sessions.put(address, session);
} else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Found device " + account.getJid().toBareJid() + ":" + ownId);
addresses.add(new AxolotlAddress(account.getJid().toBareJid().toString(), ownId));
}
}
}
}
return addresses;
}
public boolean createSessionsIfNeeded(final Conversation conversation) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Creating axolotl sessions if needed...");
boolean newSessions = false;
Set<AxolotlAddress> addresses = findDevicesWithoutSession(conversation);
for (AxolotlAddress address : addresses) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Processing device: " + address.toString());
FetchStatus status = fetchStatusMap.get(address);
if (status == null || status == FetchStatus.ERROR) {
fetchStatusMap.put(address, FetchStatus.PENDING);
this.buildSessionFromPEP(address);
newSessions = true;
} else if (status == FetchStatus.PENDING) {
newSessions = true;
} else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Already fetching bundle for " + address.toString());
}
}
return newSessions;
}
public boolean hasPendingKeyFetches(Conversation conversation) {
AxolotlAddress ownAddress = new AxolotlAddress(account.getJid().toBareJid().toString(), 0);
AxolotlAddress foreignAddress = new AxolotlAddress(conversation.getJid().toBareJid().toString(), 0);
return fetchStatusMap.getAll(ownAddress).containsValue(FetchStatus.PENDING)
|| fetchStatusMap.getAll(foreignAddress).containsValue(FetchStatus.PENDING);
}
@Nullable
private XmppAxolotlMessage buildHeader(Contact contact) {
final XmppAxolotlMessage axolotlMessage = new XmppAxolotlMessage(
contact.getJid().toBareJid(), getOwnDeviceId());
Set<XmppAxolotlSession> contactSessions = findSessionsforContact(contact);
Set<XmppAxolotlSession> ownSessions = findOwnSessions();
if (contactSessions.isEmpty()) {
return null;
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl foreign keyElements...");
for (XmppAxolotlSession session : contactSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session);
}
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building axolotl own keyElements...");
for (XmppAxolotlSession session : ownSessions) {
Log.v(Config.LOGTAG, AxolotlService.getLogprefix(account) + session.getRemoteAddress().toString());
axolotlMessage.addDevice(session);
}
return axolotlMessage;
}
@Nullable
public XmppAxolotlMessage encrypt(Message message) {
XmppAxolotlMessage axolotlMessage = buildHeader(message.getContact());
if (axolotlMessage != null) {
final String content;
if (message.hasFileOnRemoteHost()) {
content = message.getFileParams().url.toString();
} else {
content = message.getBody();
}
try {
axolotlMessage.encrypt(content);
} catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to encrypt message: " + e.getMessage());
return null;
}
}
return axolotlMessage;
}
public void preparePayloadMessage(final Message message, final boolean delay) {
executor.execute(new Runnable() {
@Override
public void run() {
XmppAxolotlMessage axolotlMessage = encrypt(message);
if (axolotlMessage == null) {
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
//mXmppConnectionService.updateConversationUi();
} else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Generated message, caching: " + message.getUuid());
messageCache.put(message.getUuid(), axolotlMessage);
mXmppConnectionService.resendMessage(message, delay);
}
}
});
}
public void prepareKeyTransportMessage(final Contact contact, final OnMessageCreatedCallback onMessageCreatedCallback) {
executor.execute(new Runnable() {
@Override
public void run() {
XmppAxolotlMessage axolotlMessage = buildHeader(contact);
onMessageCreatedCallback.run(axolotlMessage);
}
});
}
public XmppAxolotlMessage fetchAxolotlMessageFromCache(Message message) {
XmppAxolotlMessage axolotlMessage = messageCache.get(message.getUuid());
if (axolotlMessage != null) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache hit: " + message.getUuid());
messageCache.remove(message.getUuid());
} else {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Cache miss: " + message.getUuid());
}
return axolotlMessage;
}
private XmppAxolotlSession recreateUncachedSession(AxolotlAddress address) {
IdentityKey identityKey = axolotlStore.loadSession(address).getSessionState().getRemoteIdentityKey();
return (identityKey != null)
? new XmppAxolotlSession(account, axolotlStore, address,
identityKey.getFingerprint().replaceAll("\\s", ""))
: null;
}
private XmppAxolotlSession getReceivingSession(XmppAxolotlMessage message) {
AxolotlAddress senderAddress = new AxolotlAddress(message.getFrom().toString(),
message.getSenderDeviceId());
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);
}
}
return session;
}
public XmppAxolotlMessage.XmppAxolotlPlaintextMessage processReceivingPayloadMessage(XmppAxolotlMessage message) {
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = null;
XmppAxolotlSession session = getReceivingSession(message);
try {
plaintextMessage = message.decrypt(session, getOwnDeviceId());
Integer preKeyId = session.getPreKeyId();
if (preKeyId != null) {
publishBundlesIfNeeded();
session.resetPreKeyId();
}
} catch (CryptoFailedException e) {
Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage());
}
if (session.isFresh() && plaintextMessage != null) {
sessions.put(session);
}
return plaintextMessage;
}
public XmppAxolotlMessage.XmppAxolotlKeyTransportMessage processReceivingKeyTransportMessage(XmppAxolotlMessage message) {
XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage = null;
XmppAxolotlSession session = getReceivingSession(message);
keyTransportMessage = message.getParameters(session, getOwnDeviceId());
if (session.isFresh() && keyTransportMessage != null) {
sessions.put(session);
}
return keyTransportMessage;
}
}

View File

@ -0,0 +1,7 @@
package eu.siacs.conversations.crypto.axolotl;
public class CryptoFailedException extends Exception {
public CryptoFailedException(Exception e){
super(e);
}
}

View File

@ -0,0 +1,4 @@
package eu.siacs.conversations.crypto.axolotl;
public class NoSessionsCreatedException extends Throwable{
}

View File

@ -0,0 +1,5 @@
package eu.siacs.conversations.crypto.axolotl;
public interface OnMessageCreatedCallback {
void run(XmppAxolotlMessage message);
}

View File

@ -0,0 +1,421 @@
package eu.siacs.conversations.crypto.axolotl;
import android.util.Log;
import android.util.LruCache;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.state.AxolotlStore;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import org.whispersystems.libaxolotl.util.KeyHelper;
import java.util.List;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService;
public class SQLiteAxolotlStore implements AxolotlStore {
public static final String PREKEY_TABLENAME = "prekeys";
public static final String SIGNED_PREKEY_TABLENAME = "signed_prekeys";
public static final String SESSION_TABLENAME = "sessions";
public static final String IDENTITIES_TABLENAME = "identities";
public static final String ACCOUNT = "account";
public static final String DEVICE_ID = "device_id";
public static final String ID = "id";
public static final String KEY = "key";
public static final String FINGERPRINT = "fingerprint";
public static final String NAME = "name";
public static final String TRUSTED = "trusted";
public static final String OWN = "ownkey";
public static final String JSONKEY_REGISTRATION_ID = "axolotl_reg_id";
public static final String JSONKEY_CURRENT_PREKEY_ID = "axolotl_cur_prekey_id";
private static final int NUM_TRUSTS_TO_CACHE = 100;
private final Account account;
private final XmppConnectionService mXmppConnectionService;
private IdentityKeyPair identityKeyPair;
private int localRegistrationId;
private int currentPreKeyId = 0;
private final LruCache<String, XmppAxolotlSession.Trust> trustCache =
new LruCache<String, XmppAxolotlSession.Trust>(NUM_TRUSTS_TO_CACHE) {
@Override
protected XmppAxolotlSession.Trust create(String fingerprint) {
return mXmppConnectionService.databaseBackend.isIdentityKeyTrusted(account, fingerprint);
}
};
private static IdentityKeyPair generateIdentityKeyPair() {
Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl IdentityKeyPair...");
ECKeyPair identityKeyPairKeys = Curve.generateKeyPair();
return new IdentityKeyPair(new IdentityKey(identityKeyPairKeys.getPublicKey()),
identityKeyPairKeys.getPrivateKey());
}
private static int generateRegistrationId() {
Log.i(Config.LOGTAG, AxolotlService.LOGPREFIX + " : " + "Generating axolotl registration ID...");
return KeyHelper.generateRegistrationId(true);
}
public SQLiteAxolotlStore(Account account, XmppConnectionService service) {
this.account = account;
this.mXmppConnectionService = service;
this.localRegistrationId = loadRegistrationId();
this.currentPreKeyId = loadCurrentPreKeyId();
for (SignedPreKeyRecord record : loadSignedPreKeys()) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Got Axolotl signed prekey record:" + record.getId());
}
}
public int getCurrentPreKeyId() {
return currentPreKeyId;
}
// --------------------------------------
// IdentityKeyStore
// --------------------------------------
private IdentityKeyPair loadIdentityKeyPair() {
String ownName = account.getJid().toBareJid().toString();
IdentityKeyPair ownKey = mXmppConnectionService.databaseBackend.loadOwnIdentityKeyPair(account,
ownName);
if (ownKey != null) {
return ownKey;
} else {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl key for account " + ownName);
ownKey = generateIdentityKeyPair();
mXmppConnectionService.databaseBackend.storeOwnIdentityKeyPair(account, ownName, ownKey);
}
return ownKey;
}
private int loadRegistrationId() {
return loadRegistrationId(false);
}
private int loadRegistrationId(boolean regenerate) {
String regIdString = this.account.getKey(JSONKEY_REGISTRATION_ID);
int reg_id;
if (!regenerate && regIdString != null) {
reg_id = Integer.valueOf(regIdString);
} else {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve axolotl registration id for account " + account.getJid());
reg_id = generateRegistrationId();
boolean success = this.account.setKey(JSONKEY_REGISTRATION_ID, Integer.toString(reg_id));
if (success) {
mXmppConnectionService.databaseBackend.updateAccount(account);
} else {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new key to the database!");
}
}
return reg_id;
}
private int loadCurrentPreKeyId() {
String regIdString = this.account.getKey(JSONKEY_CURRENT_PREKEY_ID);
int reg_id;
if (regIdString != null) {
reg_id = Integer.valueOf(regIdString);
} else {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Could not retrieve current prekey id for account " + account.getJid());
reg_id = 0;
}
return reg_id;
}
public void regenerate() {
mXmppConnectionService.databaseBackend.wipeAxolotlDb(account);
trustCache.evictAll();
account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(0));
identityKeyPair = loadIdentityKeyPair();
localRegistrationId = loadRegistrationId(true);
currentPreKeyId = 0;
mXmppConnectionService.updateAccountUi();
}
/**
* Get the local client's identity key pair.
*
* @return The local client's persistent identity key pair.
*/
@Override
public IdentityKeyPair getIdentityKeyPair() {
if (identityKeyPair == null) {
identityKeyPair = loadIdentityKeyPair();
}
return identityKeyPair;
}
/**
* Return the local client's registration ID.
* <p/>
* Clients should maintain a registration ID, a random number
* between 1 and 16380 that's generated once at install time.
*
* @return the local client's registration ID.
*/
@Override
public int getLocalRegistrationId() {
return localRegistrationId;
}
/**
* Save a remote client's identity key
* <p/>
* Store a remote client's identity key as trusted.
*
* @param name The name of the remote client.
* @param identityKey The remote client's identity key.
*/
@Override
public void saveIdentity(String name, IdentityKey identityKey) {
if (!mXmppConnectionService.databaseBackend.loadIdentityKeys(account, name).contains(identityKey)) {
mXmppConnectionService.databaseBackend.storeIdentityKey(account, name, identityKey);
}
}
/**
* Verify a remote client's identity key.
* <p/>
* Determine whether a remote client's identity is trusted. Convention is
* that the TextSecure protocol is 'trust on first use.' This means that
* an identity key is considered 'trusted' if there is no entry for the recipient
* in the local store, or if it matches the saved key for a recipient in the local
* store. Only if it mismatches an entry in the local store is it considered
* 'untrusted.'
*
* @param name The name of the remote client.
* @param identityKey The identity key to verify.
* @return true if trusted, false if untrusted.
*/
@Override
public boolean isTrustedIdentity(String name, IdentityKey identityKey) {
return true;
}
public XmppAxolotlSession.Trust getFingerprintTrust(String fingerprint) {
return (fingerprint == null)? null : trustCache.get(fingerprint);
}
public void setFingerprintTrust(String fingerprint, XmppAxolotlSession.Trust trust) {
mXmppConnectionService.databaseBackend.setIdentityKeyTrust(account, fingerprint, trust);
trustCache.remove(fingerprint);
}
public Set<IdentityKey> getContactKeysWithTrust(String bareJid, XmppAxolotlSession.Trust trust) {
return mXmppConnectionService.databaseBackend.loadIdentityKeys(account, bareJid, trust);
}
public long getContactNumTrustedKeys(String bareJid) {
return mXmppConnectionService.databaseBackend.numTrustedKeys(account, bareJid);
}
// --------------------------------------
// SessionStore
// --------------------------------------
/**
* Returns a copy of the {@link SessionRecord} corresponding to the recipientId + deviceId tuple,
* or a new SessionRecord if one does not currently exist.
* <p/>
* It is important that implementations return a copy of the current durable information. The
* returned SessionRecord may be modified, but those changes should not have an effect on the
* durable session state (what is returned by subsequent calls to this method) without the
* store method being called here first.
*
* @param address The name and device ID of the remote client.
* @return a copy of the SessionRecord corresponding to the recipientId + deviceId tuple, or
* a new SessionRecord if one does not currently exist.
*/
@Override
public SessionRecord loadSession(AxolotlAddress address) {
SessionRecord session = mXmppConnectionService.databaseBackend.loadSession(this.account, address);
return (session != null) ? session : new SessionRecord();
}
/**
* Returns all known devices with active sessions for a recipient
*
* @param name the name of the client.
* @return all known sub-devices with active sessions.
*/
@Override
public List<Integer> getSubDeviceSessions(String name) {
return mXmppConnectionService.databaseBackend.getSubDeviceSessions(account,
new AxolotlAddress(name, 0));
}
/**
* Commit to storage the {@link SessionRecord} for a given recipientId + deviceId tuple.
*
* @param address the address of the remote client.
* @param record the current SessionRecord for the remote client.
*/
@Override
public void storeSession(AxolotlAddress address, SessionRecord record) {
mXmppConnectionService.databaseBackend.storeSession(account, address, record);
}
/**
* Determine whether there is a committed {@link SessionRecord} for a recipientId + deviceId tuple.
*
* @param address the address of the remote client.
* @return true if a {@link SessionRecord} exists, false otherwise.
*/
@Override
public boolean containsSession(AxolotlAddress address) {
return mXmppConnectionService.databaseBackend.containsSession(account, address);
}
/**
* Remove a {@link SessionRecord} for a recipientId + deviceId tuple.
*
* @param address the address of the remote client.
*/
@Override
public void deleteSession(AxolotlAddress address) {
mXmppConnectionService.databaseBackend.deleteSession(account, address);
}
/**
* Remove the {@link SessionRecord}s corresponding to all devices of a recipientId.
*
* @param name the name of the remote client.
*/
@Override
public void deleteAllSessions(String name) {
AxolotlAddress address = new AxolotlAddress(name, 0);
mXmppConnectionService.databaseBackend.deleteAllSessions(account,
address);
}
// --------------------------------------
// PreKeyStore
// --------------------------------------
/**
* Load a local PreKeyRecord.
*
* @param preKeyId the ID of the local PreKeyRecord.
* @return the corresponding PreKeyRecord.
* @throws InvalidKeyIdException when there is no corresponding PreKeyRecord.
*/
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
PreKeyRecord record = mXmppConnectionService.databaseBackend.loadPreKey(account, preKeyId);
if (record == null) {
throw new InvalidKeyIdException("No such PreKeyRecord: " + preKeyId);
}
return record;
}
/**
* Store a local PreKeyRecord.
*
* @param preKeyId the ID of the PreKeyRecord to store.
* @param record the PreKeyRecord.
*/
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
mXmppConnectionService.databaseBackend.storePreKey(account, record);
currentPreKeyId = preKeyId;
boolean success = this.account.setKey(JSONKEY_CURRENT_PREKEY_ID, Integer.toString(preKeyId));
if (success) {
mXmppConnectionService.databaseBackend.updateAccount(account);
} else {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Failed to write new prekey id to the database!");
}
}
/**
* @param preKeyId A PreKeyRecord ID.
* @return true if the store has a record for the preKeyId, otherwise false.
*/
@Override
public boolean containsPreKey(int preKeyId) {
return mXmppConnectionService.databaseBackend.containsPreKey(account, preKeyId);
}
/**
* Delete a PreKeyRecord from local storage.
*
* @param preKeyId The ID of the PreKeyRecord to remove.
*/
@Override
public void removePreKey(int preKeyId) {
mXmppConnectionService.databaseBackend.deletePreKey(account, preKeyId);
}
// --------------------------------------
// SignedPreKeyStore
// --------------------------------------
/**
* Load a local SignedPreKeyRecord.
*
* @param signedPreKeyId the ID of the local SignedPreKeyRecord.
* @return the corresponding SignedPreKeyRecord.
* @throws InvalidKeyIdException when there is no corresponding SignedPreKeyRecord.
*/
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
SignedPreKeyRecord record = mXmppConnectionService.databaseBackend.loadSignedPreKey(account, signedPreKeyId);
if (record == null) {
throw new InvalidKeyIdException("No such SignedPreKeyRecord: " + signedPreKeyId);
}
return record;
}
/**
* Load all local SignedPreKeyRecords.
*
* @return All stored SignedPreKeyRecords.
*/
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
return mXmppConnectionService.databaseBackend.loadSignedPreKeys(account);
}
/**
* Store a local SignedPreKeyRecord.
*
* @param signedPreKeyId the ID of the SignedPreKeyRecord to store.
* @param record the SignedPreKeyRecord.
*/
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
mXmppConnectionService.databaseBackend.storeSignedPreKey(account, record);
}
/**
* @param signedPreKeyId A SignedPreKeyRecord ID.
* @return true if the store has a record for the signedPreKeyId, otherwise false.
*/
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
return mXmppConnectionService.databaseBackend.containsSignedPreKey(account, signedPreKeyId);
}
/**
* Delete a SignedPreKeyRecord from local storage.
*
* @param signedPreKeyId The ID of the SignedPreKeyRecord to remove.
*/
@Override
public void removeSignedPreKey(int signedPreKeyId) {
mXmppConnectionService.databaseBackend.deleteSignedPreKey(account, signedPreKeyId);
}
}

View File

@ -0,0 +1,249 @@
package eu.siacs.conversations.crypto.axolotl;
import android.util.Base64;
import android.util.Log;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
public class XmppAxolotlMessage {
public static final String CONTAINERTAG = "encrypted";
public static final String HEADER = "header";
public static final String SOURCEID = "sid";
public static final String KEYTAG = "key";
public static final String REMOTEID = "rid";
public static final String IVTAG = "iv";
public static final String PAYLOAD = "payload";
private static final String KEYTYPE = "AES";
private static final String CIPHERMODE = "AES/GCM/NoPadding";
private static final String PROVIDER = "BC";
private byte[] innerKey;
private byte[] ciphertext = null;
private byte[] iv = null;
private final Map<Integer, byte[]> keys;
private final Jid from;
private final int sourceDeviceId;
public static class XmppAxolotlPlaintextMessage {
private final String plaintext;
private final String fingerprint;
public 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;
public 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;
}
}
private XmppAxolotlMessage(final Element axolotlMessage, final Jid from) throws IllegalArgumentException {
this.from = from;
Element header = axolotlMessage.findChild(HEADER);
this.sourceDeviceId = Integer.parseInt(header.getAttribute(SOURCEID));
List<Element> keyElements = header.getChildren();
this.keys = new HashMap<>(keyElements.size());
for (Element keyElement : keyElements) {
switch (keyElement.getName()) {
case KEYTAG:
try {
Integer recipientId = Integer.parseInt(keyElement.getAttribute(REMOTEID));
byte[] key = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
this.keys.put(recipientId, key);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e);
}
break;
case IVTAG:
if (this.iv != null) {
throw new IllegalArgumentException("Duplicate iv entry");
}
iv = Base64.decode(keyElement.getContent(), Base64.DEFAULT);
break;
default:
Log.w(Config.LOGTAG, "Unexpected element in header: " + keyElement.toString());
break;
}
}
Element payloadElement = axolotlMessage.findChild(PAYLOAD);
if (payloadElement != null) {
ciphertext = Base64.decode(payloadElement.getContent(), Base64.DEFAULT);
}
}
public XmppAxolotlMessage(Jid from, int sourceDeviceId) {
this.from = from;
this.sourceDeviceId = sourceDeviceId;
this.keys = new HashMap<>();
this.iv = generateIv();
this.innerKey = generateKey();
}
public static XmppAxolotlMessage fromElement(Element element, Jid from) {
return new XmppAxolotlMessage(element, from);
}
private static byte[] generateKey() {
try {
KeyGenerator generator = KeyGenerator.getInstance(KEYTYPE);
generator.init(128);
return generator.generateKey().getEncoded();
} catch (NoSuchAlgorithmException e) {
Log.e(Config.LOGTAG, e.getMessage());
return null;
}
}
private static byte[] generateIv() {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[16];
random.nextBytes(iv);
return iv;
}
public void encrypt(String plaintext) throws CryptoFailedException {
try {
SecretKey secretKey = new SecretKeySpec(innerKey, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
this.innerKey = secretKey.getEncoded();
this.ciphertext = cipher.doFinal(plaintext.getBytes());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| IllegalBlockSizeException | BadPaddingException | NoSuchProviderException
| InvalidAlgorithmParameterException e) {
throw new CryptoFailedException(e);
}
}
public Jid getFrom() {
return this.from;
}
public int getSenderDeviceId() {
return sourceDeviceId;
}
public byte[] getCiphertext() {
return ciphertext;
}
public void addDevice(XmppAxolotlSession session) {
byte[] key = session.processSending(innerKey);
if (key != null) {
keys.put(session.getRemoteAddress().getDeviceId(), key);
}
}
public byte[] getInnerKey() {
return innerKey;
}
public byte[] getIV() {
return this.iv;
}
public Element toElement() {
Element encryptionElement = new Element(CONTAINERTAG, AxolotlService.PEP_PREFIX);
Element headerElement = encryptionElement.addChild(HEADER);
headerElement.setAttribute(SOURCEID, sourceDeviceId);
for (Map.Entry<Integer, byte[]> keyEntry : keys.entrySet()) {
Element keyElement = new Element(KEYTAG);
keyElement.setAttribute(REMOTEID, keyEntry.getKey());
keyElement.setContent(Base64.encodeToString(keyEntry.getValue(), Base64.DEFAULT));
headerElement.addChild(keyElement);
}
headerElement.addChild(IVTAG).setContent(Base64.encodeToString(iv, Base64.DEFAULT));
if (ciphertext != null) {
Element payload = encryptionElement.addChild(PAYLOAD);
payload.setContent(Base64.encodeToString(ciphertext, Base64.DEFAULT));
}
return encryptionElement;
}
private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) {
byte[] encryptedKey = keys.get(sourceDeviceId);
return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null;
}
public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) {
byte[] key = unpackKey(session, sourceDeviceId);
return (key != null)
? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV())
: null;
}
public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException {
XmppAxolotlPlaintextMessage plaintextMessage = null;
byte[] key = unpackKey(session, sourceDeviceId);
if (key != null) {
try {
Cipher cipher = Cipher.getInstance(CIPHERMODE, PROVIDER);
SecretKeySpec keySpec = new SecretKeySpec(key, KEYTYPE);
IvParameterSpec ivSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
String plaintext = new String(cipher.doFinal(ciphertext));
plaintextMessage = new XmppAxolotlPlaintextMessage(plaintext, session.getFingerprint());
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
| InvalidAlgorithmParameterException | IllegalBlockSizeException
| BadPaddingException | NoSuchProviderException e) {
throw new CryptoFailedException(e);
}
}
return plaintextMessage;
}
}

View File

@ -0,0 +1,196 @@
package eu.siacs.conversations.crypto.axolotl;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.NoSessionException;
import org.whispersystems.libaxolotl.SessionCipher;
import org.whispersystems.libaxolotl.UntrustedIdentityException;
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
import java.util.HashMap;
import java.util.Map;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
public class XmppAxolotlSession {
private final SessionCipher cipher;
private final SQLiteAxolotlStore sqLiteAxolotlStore;
private final AxolotlAddress remoteAddress;
private final Account account;
private String fingerprint = null;
private Integer preKeyId = null;
private boolean fresh = true;
public enum Trust {
UNDECIDED(0),
TRUSTED(1),
UNTRUSTED(2),
COMPROMISED(3),
INACTIVE_TRUSTED(4),
INACTIVE_UNDECIDED(5),
INACTIVE_UNTRUSTED(6);
private static final Map<Integer, Trust> trustsByValue = new HashMap<>();
static {
for (Trust trust : Trust.values()) {
trustsByValue.put(trust.getCode(), trust);
}
}
private final int code;
Trust(int code) {
this.code = code;
}
public int getCode() {
return this.code;
}
public String toString() {
switch (this) {
case UNDECIDED:
return "Trust undecided " + getCode();
case TRUSTED:
return "Trusted " + getCode();
case COMPROMISED:
return "Compromised " + getCode();
case INACTIVE_TRUSTED:
return "Inactive (Trusted)" + getCode();
case INACTIVE_UNDECIDED:
return "Inactive (Undecided)" + getCode();
case INACTIVE_UNTRUSTED:
return "Inactive (Untrusted)" + getCode();
case UNTRUSTED:
default:
return "Untrusted " + getCode();
}
}
public static Trust fromBoolean(Boolean trusted) {
return trusted ? TRUSTED : UNTRUSTED;
}
public static Trust fromCode(int code) {
return trustsByValue.get(code);
}
}
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress, String fingerprint) {
this(account, store, remoteAddress);
this.fingerprint = fingerprint;
}
public XmppAxolotlSession(Account account, SQLiteAxolotlStore store, AxolotlAddress remoteAddress) {
this.cipher = new SessionCipher(store, remoteAddress);
this.remoteAddress = remoteAddress;
this.sqLiteAxolotlStore = store;
this.account = account;
}
public Integer getPreKeyId() {
return preKeyId;
}
public void resetPreKeyId() {
preKeyId = null;
}
public String getFingerprint() {
return fingerprint;
}
public AxolotlAddress getRemoteAddress() {
return remoteAddress;
}
public boolean isFresh() {
return fresh;
}
public void setNotFresh() {
this.fresh = false;
}
protected void setTrust(Trust trust) {
sqLiteAxolotlStore.setFingerprintTrust(fingerprint, trust);
}
protected Trust getTrust() {
Trust trust = sqLiteAxolotlStore.getFingerprintTrust(fingerprint);
return (trust == null) ? Trust.UNDECIDED : trust;
}
@Nullable
public byte[] processReceiving(byte[] encryptedKey) {
byte[] plaintext = null;
Trust trust = getTrust();
switch (trust) {
case INACTIVE_TRUSTED:
case UNDECIDED:
case UNTRUSTED:
case TRUSTED:
try {
try {
PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey);
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId());
String fingerprint = message.getIdentityKey().getFingerprint().replaceAll("\\s", "");
if (this.fingerprint != null && !this.fingerprint.equals(fingerprint)) {
Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.fingerprint + ", received message with fingerprint " + fingerprint);
} else {
this.fingerprint = fingerprint;
plaintext = cipher.decrypt(message);
if (message.getPreKeyId().isPresent()) {
preKeyId = message.getPreKeyId().get();
}
}
} catch (InvalidMessageException | InvalidVersionException e) {
Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received");
WhisperMessage message = new WhisperMessage(encryptedKey);
plaintext = cipher.decrypt(message);
} catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
}
} catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) {
Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage());
}
if (plaintext != null && trust == Trust.INACTIVE_TRUSTED) {
setTrust(Trust.TRUSTED);
}
break;
case COMPROMISED:
default:
// ignore
break;
}
return plaintext;
}
@Nullable
public byte[] processSending(@NonNull byte[] outgoingMessage) {
Trust trust = getTrust();
if (trust == Trust.TRUSTED) {
CiphertextMessage ciphertextMessage = cipher.encrypt(outgoingMessage);
return ciphertextMessage.serialize();
} else {
return null;
}
}
}

View File

@ -20,6 +20,7 @@ import java.util.concurrent.CopyOnWriteArraySet;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrService;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
@ -122,6 +123,7 @@ public class Account extends AbstractEntity {
protected String avatar;
protected boolean online = false;
private OtrService mOtrService = null;
private AxolotlService axolotlService = null;
private XmppConnection xmppConnection = null;
private long mEndGracePeriod = 0L;
private String otrFingerprint;
@ -254,6 +256,10 @@ public class Account extends AbstractEntity {
return keys;
}
public String getKey(final String name) {
return this.keys.optString(name, null);
}
public boolean setKey(final String keyName, final String keyValue) {
try {
this.keys.put(keyName, keyValue);
@ -277,8 +283,13 @@ public class Account extends AbstractEntity {
return values;
}
public AxolotlService getAxolotlService() {
return axolotlService;
}
public void initAccountServices(final XmppConnectionService context) {
this.mOtrService = new OtrService(context, this);
this.axolotlService = new AxolotlService(this, context);
}
public OtrService getOtrService() {

View File

@ -183,6 +183,7 @@ public class Contact implements ListItem, Blockable {
}
public ContentValues getContentValues() {
synchronized (this.keys) {
final ContentValues values = new ContentValues();
values.put(ACCOUNT, accountUuid);
values.put(SYSTEMNAME, systemName);
@ -198,6 +199,7 @@ public class Contact implements ListItem, Blockable {
values.put(GROUPS, groups.toString());
return values;
}
}
public int getSubscription() {
return this.subscription;
@ -281,6 +283,7 @@ public class Contact implements ListItem, Blockable {
}
public ArrayList<String> getOtrFingerprints() {
synchronized (this.keys) {
final ArrayList<String> fingerprints = new ArrayList<String>();
try {
if (this.keys.has("otr_fingerprints")) {
@ -297,8 +300,9 @@ public class Contact implements ListItem, Blockable {
}
return fingerprints;
}
}
public boolean addOtrFingerprint(String print) {
synchronized (this.keys) {
if (getOtrFingerprints().contains(print)) {
return false;
}
@ -306,7 +310,6 @@ public class Contact implements ListItem, Blockable {
JSONArray fingerprints;
if (!this.keys.has("otr_fingerprints")) {
fingerprints = new JSONArray();
} else {
fingerprints = this.keys.getJSONArray("otr_fingerprints");
}
@ -317,8 +320,10 @@ public class Contact implements ListItem, Blockable {
return false;
}
}
}
public long getPgpKeyId() {
synchronized (this.keys) {
if (this.keys.has("pgp_keyid")) {
try {
return this.keys.getLong("pgp_keyid");
@ -329,12 +334,14 @@ public class Contact implements ListItem, Blockable {
return 0;
}
}
}
public void setPgpKeyId(long keyId) {
synchronized (this.keys) {
try {
this.keys.put("pgp_keyid", keyId);
} catch (final JSONException ignored) {
}
}
}
@ -441,6 +448,7 @@ public class Contact implements ListItem, Blockable {
}
public boolean deleteOtrFingerprint(String fingerprint) {
synchronized (this.keys) {
boolean success = false;
try {
if (this.keys.has("otr_fingerprints")) {
@ -461,6 +469,7 @@ public class Contact implements ListItem, Blockable {
return false;
}
}
}
public boolean trusted() {
return getOption(Options.FROM) && getOption(Options.TO);

View File

@ -179,11 +179,11 @@ public class Conversation extends AbstractEntity implements Blockable {
}
}
public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) {
public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
synchronized (this.messages) {
for (Message message : this.messages) {
if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
&& (message.getEncryption() == Message.ENCRYPTION_OTR)) {
&& (message.getEncryption() == encryptionType)) {
onMessageFound.onMessageFound(message);
}
}
@ -519,6 +519,13 @@ public class Conversation extends AbstractEntity implements Blockable {
return getContact().getOtrFingerprints().contains(getOtrFingerprint());
}
/**
* short for is Private and Non-anonymous
*/
public boolean isPnNA() {
return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
}
public synchronized MucOptions getMucOptions() {
if (this.mucOptions == null) {
this.mucOptions = new MucOptions(this);
@ -542,43 +549,52 @@ public class Conversation extends AbstractEntity implements Blockable {
return this.nextCounterpart;
}
public int getLatestEncryption() {
int latestEncryption = this.getLatestMessage().getEncryption();
if ((latestEncryption == Message.ENCRYPTION_DECRYPTED)
|| (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) {
private int getMostRecentlyUsedOutgoingEncryption() {
synchronized (this.messages) {
for(int i = this.messages.size() -1; i >= 0; --i) {
final Message m = this.messages.get(0);
if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) {
final int e = m.getEncryption();
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
return Message.ENCRYPTION_PGP;
} else {
return latestEncryption;
return e;
}
}
}
}
return Message.ENCRYPTION_NONE;
}
public int getNextEncryption(boolean force) {
private int getMostRecentlyUsedIncomingEncryption() {
synchronized (this.messages) {
for(int i = this.messages.size() -1; i >= 0; --i) {
final Message m = this.messages.get(0);
if (m.getStatus() == Message.STATUS_RECEIVED) {
final int e = m.getEncryption();
if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
return Message.ENCRYPTION_PGP;
} else {
return e;
}
}
}
}
return Message.ENCRYPTION_NONE;
}
public int getNextEncryption() {
int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
if (next == -1) {
int latest = this.getLatestEncryption();
if (latest == Message.ENCRYPTION_NONE) {
if (force && getMode() == MODE_SINGLE) {
return Message.ENCRYPTION_OTR;
} else if (getContact().getPresences().size() == 1) {
if (getContact().getOtrFingerprints().size() >= 1) {
return Message.ENCRYPTION_OTR;
int outgoing = this.getMostRecentlyUsedOutgoingEncryption();
if (outgoing == Message.ENCRYPTION_NONE) {
return this.getMostRecentlyUsedIncomingEncryption();
} else {
return latest;
}
} else {
return latest;
}
} else {
return latest;
return outgoing;
}
}
if (next == Message.ENCRYPTION_NONE && force
&& getMode() == MODE_SINGLE) {
return Message.ENCRYPTION_OTR;
} else {
return next;
}
}
public void setNextEncryption(int encryption) {
this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));

View File

@ -1,37 +1,16 @@
package eu.siacs.conversations.entities;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URLConnection;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
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 eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.MimeUtils;
import android.util.Log;
public class DownloadableFile extends File {
private static final long serialVersionUID = 2247012619505115863L;
private long expectedSize = 0;
private String sha1sum;
private Key aeskey;
private String mime;
private byte[] aeskey;
private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
@ -45,16 +24,8 @@ public class DownloadableFile extends File {
}
public long getExpectedSize() {
if (this.aeskey != null) {
if (this.expectedSize == 0) {
return 0;
} else {
return (this.expectedSize / 16 + 1) * 16;
}
} else {
return this.expectedSize;
}
}
public String getMimeType() {
String path = this.getAbsolutePath();
@ -79,91 +50,38 @@ public class DownloadableFile extends File {
this.sha1sum = sum;
}
public void setKey(byte[] key) {
if (key.length == 48) {
public void setKeyAndIv(byte[] keyIvCombo) {
if (keyIvCombo.length == 48) {
byte[] secretKey = new byte[32];
byte[] iv = new byte[16];
System.arraycopy(key, 0, iv, 0, 16);
System.arraycopy(key, 16, secretKey, 0, 32);
this.aeskey = new SecretKeySpec(secretKey, "AES");
System.arraycopy(keyIvCombo, 0, iv, 0, 16);
System.arraycopy(keyIvCombo, 16, secretKey, 0, 32);
this.aeskey = secretKey;
this.iv = iv;
} else if (key.length >= 32) {
} else if (keyIvCombo.length >= 32) {
byte[] secretKey = new byte[32];
System.arraycopy(key, 0, secretKey, 0, 32);
this.aeskey = new SecretKeySpec(secretKey, "AES");
} else if (key.length >= 16) {
System.arraycopy(keyIvCombo, 0, secretKey, 0, 32);
this.aeskey = secretKey;
} else if (keyIvCombo.length >= 16) {
byte[] secretKey = new byte[16];
System.arraycopy(key, 0, secretKey, 0, 16);
this.aeskey = new SecretKeySpec(secretKey, "AES");
System.arraycopy(keyIvCombo, 0, secretKey, 0, 16);
this.aeskey = secretKey;
}
}
public Key getKey() {
public void setKey(byte[] key) {
this.aeskey = key;
}
public void setIv(byte[] iv) {
this.iv = iv;
}
public byte[] getKey() {
return this.aeskey;
}
public InputStream createInputStream() {
if (this.getKey() == null) {
try {
return new FileInputStream(this);
} catch (FileNotFoundException e) {
return null;
}
} else {
try {
IvParameterSpec ips = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips);
Log.d(Config.LOGTAG, "opening encrypted input stream");
return new CipherInputStream(new FileInputStream(this), cipher);
} catch (NoSuchAlgorithmException e) {
Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
return null;
} catch (NoSuchPaddingException e) {
Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
return null;
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
return null;
} catch (InvalidAlgorithmParameterException e) {
Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
return null;
} catch (FileNotFoundException e) {
return null;
}
}
}
public OutputStream createOutputStream() {
if (this.getKey() == null) {
try {
return new FileOutputStream(this);
} catch (FileNotFoundException e) {
return null;
}
} else {
try {
IvParameterSpec ips = new IvParameterSpec(this.iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips);
Log.d(Config.LOGTAG, "opening encrypted output stream");
return new CipherOutputStream(new FileOutputStream(this),
cipher);
} catch (NoSuchAlgorithmException e) {
Log.d(Config.LOGTAG, "no such algo: " + e.getMessage());
return null;
} catch (NoSuchPaddingException e) {
Log.d(Config.LOGTAG, "no such padding: " + e.getMessage());
return null;
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, "invalid key: " + e.getMessage());
return null;
} catch (InvalidAlgorithmParameterException e) {
Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage());
return null;
} catch (FileNotFoundException e) {
return null;
}
}
public byte[] getIv() {
return this.iv;
}
}

View File

@ -8,6 +8,7 @@ import java.net.URL;
import java.util.Arrays;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.MimeUtils;
import eu.siacs.conversations.utils.UIHelper;
@ -34,6 +35,7 @@ public class Message extends AbstractEntity {
public static final int ENCRYPTION_OTR = 2;
public static final int ENCRYPTION_DECRYPTED = 3;
public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
public static final int ENCRYPTION_AXOLOTL = 5;
public static final int TYPE_TEXT = 0;
public static final int TYPE_IMAGE = 1;
@ -49,9 +51,11 @@ public class Message extends AbstractEntity {
public static final String ENCRYPTION = "encryption";
public static final String STATUS = "status";
public static final String TYPE = "type";
public static final String CARBON = "carbon";
public static final String REMOTE_MSG_ID = "remoteMsgId";
public static final String SERVER_MSG_ID = "serverMsgId";
public static final String RELATIVE_FILE_PATH = "relativeFilePath";
public static final String FINGERPRINT = "axolotl_fingerprint";
public static final String ME_COMMAND = "/me ";
@ -65,6 +69,7 @@ public class Message extends AbstractEntity {
protected int encryption;
protected int status;
protected int type;
protected boolean carbon = false;
protected String relativeFilePath;
protected boolean read = true;
protected String remoteMsgId = null;
@ -73,6 +78,7 @@ public class Message extends AbstractEntity {
protected Transferable transferable = null;
private Message mNextMessage = null;
private Message mPreviousMessage = null;
private String axolotlFingerprint = null;
private Message() {
@ -81,8 +87,11 @@ public class Message extends AbstractEntity {
public Message(Conversation conversation, String body, int encryption) {
this(conversation, body, encryption, STATUS_UNSEND);
}
public Message(Conversation conversation, String body, int encryption, int status) {
this(conversation, body, encryption, status, false);
}
public Message(Conversation conversation, String body, int encryption, int status, boolean carbon) {
this(java.util.UUID.randomUUID().toString(),
conversation.getUuid(),
conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
@ -92,6 +101,8 @@ public class Message extends AbstractEntity {
encryption,
status,
TYPE_TEXT,
false,
null,
null,
null,
null);
@ -100,8 +111,9 @@ public class Message extends AbstractEntity {
private Message(final String uuid, final String conversationUUid, final Jid counterpart,
final Jid trueCounterpart, final String body, final long timeSent,
final int encryption, final int status, final int type, final String remoteMsgId,
final String relativeFilePath, final String serverMsgId) {
final int encryption, final int status, final int type, final boolean carbon,
final String remoteMsgId, final String relativeFilePath,
final String serverMsgId, final String fingerprint) {
this.uuid = uuid;
this.conversationUuid = conversationUUid;
this.counterpart = counterpart;
@ -111,9 +123,11 @@ public class Message extends AbstractEntity {
this.encryption = encryption;
this.status = status;
this.type = type;
this.carbon = carbon;
this.remoteMsgId = remoteMsgId;
this.relativeFilePath = relativeFilePath;
this.serverMsgId = serverMsgId;
this.axolotlFingerprint = fingerprint;
}
public static Message fromCursor(Cursor cursor) {
@ -148,9 +162,11 @@ public class Message extends AbstractEntity {
cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
cursor.getInt(cursor.getColumnIndex(STATUS)),
cursor.getInt(cursor.getColumnIndex(TYPE)),
cursor.getInt(cursor.getColumnIndex(CARBON))>0,
cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)));
cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
cursor.getString(cursor.getColumnIndex(FINGERPRINT)));
}
public static Message createStatusMessage(Conversation conversation, String body) {
@ -181,9 +197,11 @@ public class Message extends AbstractEntity {
values.put(ENCRYPTION, encryption);
values.put(STATUS, status);
values.put(TYPE, type);
values.put(CARBON, carbon ? 1 : 0);
values.put(REMOTE_MSG_ID, remoteMsgId);
values.put(RELATIVE_FILE_PATH, relativeFilePath);
values.put(SERVER_MSG_ID, serverMsgId);
values.put(FINGERPRINT, axolotlFingerprint);
return values;
}
@ -304,6 +322,14 @@ public class Message extends AbstractEntity {
this.type = type;
}
public boolean isCarbon() {
return carbon;
}
public void setCarbon(boolean carbon) {
this.carbon = carbon;
}
public void setTrueCounterpart(Jid trueCounterpart) {
this.trueCounterpart = trueCounterpart;
}
@ -391,7 +417,8 @@ public class Message extends AbstractEntity {
!message.getBody().startsWith(ME_COMMAND) &&
!this.getBody().startsWith(ME_COMMAND) &&
!this.bodyIsHeart() &&
!message.bodyIsHeart()
!message.bodyIsHeart() &&
this.isTrusted() == message.isTrusted()
);
}
@ -407,11 +434,14 @@ public class Message extends AbstractEntity {
}
public String getMergedBody() {
final Message next = this.next();
if (this.mergeable(next)) {
return getBody().trim() + MERGE_SEPARATOR + next.getMergedBody();
StringBuilder body = new StringBuilder(this.body.trim());
Message current = this;
while(current.mergeable(current.next())) {
current = current.next();
body.append(MERGE_SEPARATOR);
body.append(current.getBody().trim());
}
return getBody().trim();
return body.toString();
}
public boolean hasMeCommand() {
@ -419,20 +449,23 @@ public class Message extends AbstractEntity {
}
public int getMergedStatus() {
final Message next = this.next();
if (this.mergeable(next)) {
return next.getStatus();
int status = this.status;
Message current = this;
while(current.mergeable(current.next())) {
current = current.next();
status = current.status;
}
return getStatus();
return status;
}
public long getMergedTimeSent() {
Message next = this.next();
if (this.mergeable(next)) {
return next.getMergedTimeSent();
} else {
return getTimeSent();
long time = this.timeSent;
Message current = this;
while(current.mergeable(current.next())) {
current = current.next();
time = current.timeSent;
}
return time;
}
public boolean wasMergedIntoPrevious() {
@ -663,4 +696,48 @@ public class Message extends AbstractEntity {
public int width = 0;
public int height = 0;
}
public void setAxolotlFingerprint(String fingerprint) {
this.axolotlFingerprint = fingerprint;
}
public String getAxolotlFingerprint() {
return axolotlFingerprint;
}
public boolean isTrusted() {
return conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint)
== XmppAxolotlSession.Trust.TRUSTED;
}
private int getPreviousEncryption() {
for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
continue;
}
return iterator.getEncryption();
}
return ENCRYPTION_NONE;
}
private int getNextEncryption() {
for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
continue;
}
return iterator.getEncryption();
}
return conversation.getNextEncryption();
}
public boolean isValidInSession() {
int pastEncryption = this.getPreviousEncryption();
int futureEncryption = this.getNextEncryption();
boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
|| futureEncryption == ENCRYPTION_NONE
|| pastEncryption != futureEncryption;
return inUnencryptedSession || this.getEncryption() == pastEncryption;
}
}

View File

@ -1,5 +1,7 @@
package eu.siacs.conversations.entities;
import android.annotation.SuppressLint;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
@ -11,8 +13,6 @@ import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.PresencePacket;
import android.annotation.SuppressLint;
@SuppressLint("DefaultLocale")
public class MucOptions {
@ -264,6 +264,15 @@ public class MucOptions {
users.add(user);
}
public boolean isUserInRoom(String name) {
for (int i = 0; i < users.size(); ++i) {
if (users.get(i).getName().equals(name)) {
return true;
}
}
return false;
}
public void processPacket(PresencePacket packet, PgpEngine pgp) {
final Jid from = packet.getFrom();
if (!from.isBareJid()) {

View File

@ -12,6 +12,7 @@ import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.PhoneHelper;
@ -28,7 +29,8 @@ public abstract class AbstractGenerator {
"urn:xmpp:avatar:metadata+notify",
"urn:xmpp:ping",
"jabber:iq:version",
"http://jabber.org/protocol/chatstates"};
"http://jabber.org/protocol/chatstates",
AxolotlService.PEP_DEVICE_LIST+"+notify"};
private final String[] MESSAGE_CONFIRMATION_FEATURES = {
"urn:xmpp:chat-markers:0",
"urn:xmpp:receipts"

View File

@ -1,15 +1,23 @@
package eu.siacs.conversations.generator;
import android.util.Base64;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.services.MessageArchiveService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.PhoneHelper;
import eu.siacs.conversations.utils.Xmlns;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.forms.Data;
@ -115,6 +123,56 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket retrieveDeviceIds(final Jid to) {
final IqPacket packet = retrieve(AxolotlService.PEP_DEVICE_LIST, null);
if(to != null) {
packet.setTo(to);
}
return packet;
}
public IqPacket retrieveBundlesForDevice(final Jid to, final int deviceid) {
final IqPacket packet = retrieve(AxolotlService.PEP_BUNDLES+":"+deviceid, null);
if(to != null) {
packet.setTo(to);
}
return packet;
}
public IqPacket publishDeviceIds(final Set<Integer> ids) {
final Element item = new Element("item");
final Element list = item.addChild("list", AxolotlService.PEP_PREFIX);
for(Integer id:ids) {
final Element device = new Element("device");
device.setAttribute("id", id);
list.addChild(device);
}
return publish(AxolotlService.PEP_DEVICE_LIST, item);
}
public IqPacket publishBundles(final SignedPreKeyRecord signedPreKeyRecord, final IdentityKey identityKey,
final Set<PreKeyRecord> preKeyRecords, final int deviceId) {
final Element item = new Element("item");
final Element bundle = item.addChild("bundle", AxolotlService.PEP_PREFIX);
final Element signedPreKeyPublic = bundle.addChild("signedPreKeyPublic");
signedPreKeyPublic.setAttribute("signedPreKeyId", signedPreKeyRecord.getId());
ECPublicKey publicKey = signedPreKeyRecord.getKeyPair().getPublicKey();
signedPreKeyPublic.setContent(Base64.encodeToString(publicKey.serialize(),Base64.DEFAULT));
final Element signedPreKeySignature = bundle.addChild("signedPreKeySignature");
signedPreKeySignature.setContent(Base64.encodeToString(signedPreKeyRecord.getSignature(),Base64.DEFAULT));
final Element identityKeyElement = bundle.addChild("identityKey");
identityKeyElement.setContent(Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
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));
}
return publish(AxolotlService.PEP_BUNDLES+":"+deviceId, item);
}
public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) {
final IqPacket packet = new IqPacket(IqPacket.TYPE.SET);
final Element query = packet.query("urn:xmpp:mam:0");
@ -196,12 +254,15 @@ public class IqGenerator extends AbstractGenerator {
return packet;
}
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file) {
public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) {
IqPacket packet = new IqPacket(IqPacket.TYPE.GET);
packet.setTo(host);
Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD);
request.addChild("filename").setContent(file.getName());
request.addChild("size").setContent(String.valueOf(file.getExpectedSize()));
if (mime != null) {
request.addChild("content-type", mime);
}
return packet;
}
}

View File

@ -1,12 +1,14 @@
package eu.siacs.conversations.generator;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import net.java.otr4j.OtrException;
import net.java.otr4j.session.Session;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
@ -21,7 +23,7 @@ public class MessageGenerator extends AbstractGenerator {
super(service);
}
private MessagePacket preparePacket(Message message, boolean addDelay) {
private MessagePacket preparePacket(Message message) {
Conversation conversation = message.getConversation();
Account account = conversation.getAccount();
MessagePacket packet = new MessagePacket();
@ -44,13 +46,10 @@ public class MessageGenerator extends AbstractGenerator {
}
packet.setFrom(account.getJid());
packet.setId(message.getUuid());
if (addDelay) {
addDelay(packet, message.getTimeSent());
}
return packet;
}
private void addDelay(MessagePacket packet, long timestamp) {
public void addDelay(MessagePacket packet, long timestamp) {
final SimpleDateFormat mDateFormat = new SimpleDateFormat(
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
@ -59,16 +58,21 @@ public class MessageGenerator extends AbstractGenerator {
delay.setAttribute("stamp", mDateFormat.format(date));
}
public MessagePacket generateOtrChat(Message message) {
return generateOtrChat(message, false);
public MessagePacket generateAxolotlChat(Message message, XmppAxolotlMessage axolotlMessage) {
MessagePacket packet = preparePacket(message);
if (axolotlMessage == null) {
return null;
}
packet.setAxolotlMessage(axolotlMessage.toElement());
return packet;
}
public MessagePacket generateOtrChat(Message message, boolean addDelay) {
public MessagePacket generateOtrChat(Message message) {
Session otrSession = message.getConversation().getOtrSession();
if (otrSession == null) {
return null;
}
MessagePacket packet = preparePacket(message, addDelay);
MessagePacket packet = preparePacket(message);
packet.addChild("private", "urn:xmpp:carbons:2");
packet.addChild("no-copy", "urn:xmpp:hints");
packet.addChild("no-permanent-store", "urn:xmpp:hints");
@ -88,11 +92,7 @@ public class MessageGenerator extends AbstractGenerator {
}
public MessagePacket generateChat(Message message) {
return generateChat(message, false);
}
public MessagePacket generateChat(Message message, boolean addDelay) {
MessagePacket packet = preparePacket(message, addDelay);
MessagePacket packet = preparePacket(message);
if (message.hasFileOnRemoteHost()) {
packet.setBody(message.getFileParams().url.toString());
} else {
@ -102,11 +102,7 @@ public class MessageGenerator extends AbstractGenerator {
}
public MessagePacket generatePgpChat(Message message) {
return generatePgpChat(message, false);
}
public MessagePacket generatePgpChat(Message message, boolean addDelay) {
MessagePacket packet = preparePacket(message, addDelay);
MessagePacket packet = preparePacket(message);
packet.setBody("This is an XEP-0027 encrypted message");
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
packet.addChild("x", "jabber:x:encrypted").setContent(message.getEncryptedBody());
@ -119,25 +115,26 @@ public class MessageGenerator extends AbstractGenerator {
public MessagePacket generateChatState(Conversation conversation) {
final Account account = conversation.getAccount();
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_CHAT);
packet.setTo(conversation.getJid().toBareJid());
packet.setFrom(account.getJid());
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
packet.addChild("no-store", "urn:xmpp:hints");
packet.addChild("no-storage", "urn:xmpp:hints"); //wrong! don't copy this. Its *store*
return packet;
}
public MessagePacket confirm(final Account account, final Jid to, final String id) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_NORMAL);
packet.setType(MessagePacket.TYPE_CHAT);
packet.setTo(to);
packet.setFrom(account.getJid());
Element received = packet.addChild("displayed",
"urn:xmpp:chat-markers:0");
Element received = packet.addChild("displayed","urn:xmpp:chat-markers:0");
received.setAttribute("id", id);
return packet;
}
public MessagePacket conferenceSubject(Conversation conversation,
String subject) {
public MessagePacket conferenceSubject(Conversation conversation,String subject) {
MessagePacket packet = new MessagePacket();
packet.setType(MessagePacket.TYPE_GROUPCHAT);
packet.setTo(conversation.getJid().toBareJid());
@ -171,10 +168,9 @@ public class MessageGenerator extends AbstractGenerator {
return packet;
}
public MessagePacket received(Account account,
MessagePacket originalMessage, String namespace) {
public MessagePacket received(Account account, MessagePacket originalMessage, String namespace, int type) {
MessagePacket receivedPacket = new MessagePacket();
receivedPacket.setType(MessagePacket.TYPE_NORMAL);
receivedPacket.setType(type);
receivedPacket.setTo(originalMessage.getFrom());
receivedPacket.setFrom(account.getJid());
Element received = receivedPacket.addChild("received", namespace);

View File

@ -38,9 +38,9 @@ public class HttpConnectionManager extends AbstractConnectionManager {
return connection;
}
public HttpUploadConnection createNewUploadConnection(Message message) {
public HttpUploadConnection createNewUploadConnection(Message message, boolean delay) {
HttpUploadConnection connection = new HttpUploadConnection(this);
connection.init(message);
connection.init(message,delay);
this.uploadConnections.add(connection);
return connection;
}

View File

@ -2,11 +2,12 @@ package eu.siacs.conversations.http;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.os.PowerManager;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
@ -18,9 +19,11 @@ import javax.net.ssl.SSLHandshakeException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
@ -35,7 +38,6 @@ public class HttpDownloadConnection implements Transferable {
private int mStatus = Transferable.STATUS_UNKNOWN;
private boolean acceptedAutomatically = false;
private int mProgress = 0;
private long mLastGuiRefresh = 0;
public HttpDownloadConnection(HttpConnectionManager manager) {
this.mHttpConnectionManager = manager;
@ -70,7 +72,8 @@ public class HttpDownloadConnection implements Transferable {
String secondToLast = parts.length >= 2 ? parts[parts.length -2] : null;
if ("pgp".equals(lastPart) || "gpg".equals(lastPart)) {
this.message.setEncryption(Message.ENCRYPTION_PGP);
} else if (message.getEncryption() != Message.ENCRYPTION_OTR) {
} else if (message.getEncryption() != Message.ENCRYPTION_OTR
&& message.getEncryption() != Message.ENCRYPTION_AXOLOTL) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
String extension;
@ -83,10 +86,11 @@ public class HttpDownloadConnection implements Transferable {
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
String reference = mUrl.getRef();
if (reference != null && reference.length() == 96) {
this.file.setKey(CryptoHelper.hexToBytes(reference));
this.file.setKeyAndIv(CryptoHelper.hexToBytes(reference));
}
if (this.message.getEncryption() == Message.ENCRYPTION_OTR
if ((this.message.getEncryption() == Message.ENCRYPTION_OTR
|| this.message.getEncryption() == Message.ENCRYPTION_AXOLOTL)
&& this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
@ -123,6 +127,17 @@ public class HttpDownloadConnection implements Transferable {
mXmppConnectionService.updateConversationUi();
}
private void showToastForException(Exception e) {
e.printStackTrace();
if (e instanceof java.net.UnknownHostException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_server_not_found);
} else if (e instanceof java.net.ConnectException) {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_could_not_connect);
} else {
mXmppConnectionService.showErrorToastInUi(R.string.download_failed_file_not_found);
}
}
private class FileSizeChecker implements Runnable {
private boolean interactive = false;
@ -144,7 +159,7 @@ public class HttpDownloadConnection implements Transferable {
} catch (IOException e) {
Log.d(Config.LOGTAG, "io exception in http file size checker: " + e.getMessage());
if (interactive) {
mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
showToastForException(e);
}
cancel();
return;
@ -161,7 +176,8 @@ public class HttpDownloadConnection implements Transferable {
}
private long retrieveFileSize() throws IOException {
Log.d(Config.LOGTAG,"retrieve file size. interactive:"+String.valueOf(interactive));
try {
Log.d(Config.LOGTAG, "retrieve file size. interactive:" + String.valueOf(interactive));
changeStatus(STATUS_CHECKING);
HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
connection.setRequestMethod("HEAD");
@ -170,11 +186,13 @@ public class HttpDownloadConnection implements Transferable {
}
connection.connect();
String contentLength = connection.getHeaderField("Content-Length");
connection.disconnect();
if (contentLength == null) {
throw new IOException();
}
try {
return Long.parseLong(contentLength, 10);
} catch (IOException e) {
throw e;
} catch (NumberFormatException e) {
throw new IOException();
}
@ -186,6 +204,8 @@ public class HttpDownloadConnection implements Transferable {
private boolean interactive = false;
private OutputStream os;
public FileDownloader(boolean interactive) {
this.interactive = interactive;
}
@ -200,24 +220,27 @@ public class HttpDownloadConnection implements Transferable {
} catch (SSLHandshakeException e) {
changeStatus(STATUS_OFFER);
} catch (IOException e) {
mXmppConnectionService.showErrorToastInUi(R.string.file_not_found_on_remote_host);
if (interactive) {
showToastForException(e);
}
cancel();
}
}
private void download() throws SSLHandshakeException, IOException {
private void download() throws IOException {
InputStream is = null;
PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_download_"+message.getUuid());
try {
wakeLock.acquire();
HttpURLConnection connection = (HttpURLConnection) mUrl.openConnection();
if (connection instanceof HttpsURLConnection) {
mHttpConnectionManager.setupTrustManager((HttpsURLConnection) connection, interactive);
}
connection.connect();
BufferedInputStream is = new BufferedInputStream(connection.getInputStream());
is = new BufferedInputStream(connection.getInputStream());
file.getParentFile().mkdirs();
file.createNewFile();
OutputStream os = file.createOutputStream();
if (os == null) {
throw new IOException();
}
os = AbstractConnectionManager.createOutputStream(file, true);
long transmitted = 0;
long expected = file.getExpectedSize();
int count = -1;
@ -228,8 +251,13 @@ public class HttpDownloadConnection implements Transferable {
updateProgress((int) ((((double) transmitted) / expected) * 100));
}
os.flush();
os.close();
is.close();
} catch (IOException e) {
throw e;
} finally {
FileBackend.close(os);
FileBackend.close(is);
wakeLock.release();
}
}
private void updateImageBounds() {
@ -242,11 +270,8 @@ public class HttpDownloadConnection implements Transferable {
public void updateProgress(int i) {
this.mProgress = i;
if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
this.mLastGuiRefresh = SystemClock.elapsedRealtime();
mXmppConnectionService.updateConversationUi();
}
}
@Override
public int getStatus() {

View File

@ -1,8 +1,13 @@
package eu.siacs.conversations.http;
import android.app.PendingIntent;
import android.content.Intent;
import android.net.Uri;
import android.os.PowerManager;
import android.util.Log;
import android.util.Pair;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -14,10 +19,11 @@ import javax.net.ssl.HttpsURLConnection;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.utils.CryptoHelper;
@ -33,16 +39,19 @@ public class HttpUploadConnection implements Transferable {
private XmppConnectionService mXmppConnectionService;
private boolean canceled = false;
private boolean delayed = false;
private Account account;
private DownloadableFile file;
private Message message;
private String mime;
private URL mGetUrl;
private URL mPutUrl;
private byte[] key = null;
private long transmitted = 0;
private long expected = 1;
private InputStream mFileInputStream;
public HttpUploadConnection(HttpConnectionManager httpConnectionManager) {
this.mHttpConnectionManager = httpConnectionManager;
@ -66,7 +75,7 @@ public class HttpUploadConnection implements Transferable {
@Override
public int getProgress() {
return (int) ((((double) transmitted) / expected) * 100);
return (int) ((((double) transmitted) / file.getExpectedSize()) * 100);
}
@Override
@ -77,25 +86,36 @@ public class HttpUploadConnection implements Transferable {
private void fail() {
mHttpConnectionManager.finishUploadConnection(this);
message.setTransferable(null);
mXmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED);
mXmppConnectionService.markMessage(message, Message.STATUS_SEND_FAILED);
FileBackend.close(mFileInputStream);
}
public void init(Message message) {
public void init(Message message, boolean delay) {
this.message = message;
message.setTransferable(this);
mXmppConnectionService.markMessage(message,Message.STATUS_UNSEND);
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
this.account = message.getConversation().getAccount();
this.file = mXmppConnectionService.getFileBackend().getFile(message, false);
this.file.setExpectedSize(this.file.getSize());
if (Config.ENCRYPT_ON_HTTP_UPLOADED) {
this.mime = this.file.getMimeType();
this.delayed = delay;
if (Config.ENCRYPT_ON_HTTP_UPLOADED
|| message.getEncryption() == Message.ENCRYPTION_AXOLOTL
|| message.getEncryption() == Message.ENCRYPTION_OTR) {
this.key = new byte[48];
mXmppConnectionService.getRNG().nextBytes(this.key);
this.file.setKey(this.key);
this.file.setKeyAndIv(this.key);
}
Pair<InputStream,Integer> pair;
try {
pair = AbstractConnectionManager.createInputStream(file, true);
} catch (FileNotFoundException e) {
fail();
return;
}
this.file.setExpectedSize(pair.second);
this.mFileInputStream = pair.first;
Jid host = account.getXmppConnection().findDiscoItemByFeature(Xmlns.HTTP_UPLOAD);
IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file);
IqPacket request = mXmppConnectionService.getIqGenerator().requestHttpUploadSlot(host,file,mime);
mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
@ -130,9 +150,10 @@ public class HttpUploadConnection implements Transferable {
private void upload() {
OutputStream os = null;
InputStream is = null;
HttpURLConnection connection = null;
PowerManager.WakeLock wakeLock = mHttpConnectionManager.createWakeLock("http_upload_"+message.getUuid());
try {
wakeLock.acquire();
Log.d(Config.LOGTAG, "uploading to " + mPutUrl.toString());
connection = (HttpURLConnection) mPutUrl.openConnection();
if (connection instanceof HttpsURLConnection) {
@ -140,37 +161,38 @@ public class HttpUploadConnection implements Transferable {
}
connection.setRequestMethod("PUT");
connection.setFixedLengthStreamingMode((int) file.getExpectedSize());
connection.setRequestProperty("Content-Type", mime == null ? "application/octet-stream" : mime);
connection.setDoOutput(true);
connection.connect();
os = connection.getOutputStream();
is = file.createInputStream();
transmitted = 0;
expected = file.getExpectedSize();
int count = -1;
byte[] buffer = new byte[4096];
while (((count = is.read(buffer)) != -1) && !canceled) {
while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) {
transmitted += count;
os.write(buffer, 0, count);
mXmppConnectionService.updateConversationUi();
}
os.flush();
os.close();
is.close();
mFileInputStream.close();
int code = connection.getResponseCode();
if (code == 200 || code == 201) {
Log.d(Config.LOGTAG, "finished uploading file");
Message.FileParams params = message.getFileParams();
if (key != null) {
mGetUrl = new URL(mGetUrl.toString() + "#" + CryptoHelper.bytesToHex(key));
}
mXmppConnectionService.getFileBackend().updateFileParams(message, mGetUrl);
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
mXmppConnectionService.sendBroadcast(intent);
message.setTransferable(null);
message.setCounterpart(message.getConversation().getJid().toBareJid());
if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
mXmppConnectionService.getPgpEngine().encrypt(message, new UiCallback<Message>() {
@Override
public void success(Message message) {
mXmppConnectionService.resendMessage(message);
mXmppConnectionService.resendMessage(message,delayed);
}
@Override
@ -184,20 +206,22 @@ public class HttpUploadConnection implements Transferable {
}
});
} else {
mXmppConnectionService.resendMessage(message);
mXmppConnectionService.resendMessage(message, delayed);
}
} else {
fail();
}
} catch (IOException e) {
Log.d(Config.LOGTAG, e.getMessage());
e.printStackTrace();
Log.d(Config.LOGTAG,"http upload failed "+e.getMessage());
fail();
} finally {
FileBackend.close(is);
FileBackend.close(mFileInputStream);
FileBackend.close(os);
if (connection != null) {
connection.disconnect();
}
wakeLock.release();
}
}
}

View File

@ -11,7 +11,6 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
public abstract class AbstractParser {

View File

@ -1,11 +1,25 @@
package eu.siacs.conversations.parser;
import android.support.annotation.NonNull;
import android.util.Base64;
import android.util.Log;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.state.PreKeyBundle;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.services.XmppConnectionService;
@ -71,6 +85,155 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived {
return super.avatarData(items);
}
public Element getItem(final IqPacket packet) {
final Element pubsub = packet.findChild("pubsub",
"http://jabber.org/protocol/pubsub");
if (pubsub == null) {
return null;
}
final Element items = pubsub.findChild("items");
if (items == null) {
return null;
}
return items.findChild("item");
}
@NonNull
public Set<Integer> deviceIds(final Element item) {
Set<Integer> deviceIds = new HashSet<>();
if (item != null) {
final Element list = item.findChild("list");
if (list != null) {
for (Element device : list.getChildren()) {
if (!device.getName().equals("device")) {
continue;
}
try {
Integer id = Integer.valueOf(device.getAttribute("id"));
deviceIds.add(id);
} catch (NumberFormatException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered nvalid <device> node in PEP:" + device.toString()
+ ", skipping...");
continue;
}
}
}
}
return deviceIds;
}
public Integer signedPreKeyId(final Element bundle) {
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if(signedPreKeyPublic == null) {
return null;
}
return Integer.valueOf(signedPreKeyPublic.getAttribute("signedPreKeyId"));
}
public ECPublicKey signedPreKeyPublic(final Element bundle) {
ECPublicKey publicKey = null;
final Element signedPreKeyPublic = bundle.findChild("signedPreKeyPublic");
if(signedPreKeyPublic == null) {
return null;
}
try {
publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage());
}
return publicKey;
}
public byte[] signedPreKeySignature(final Element bundle) {
final Element signedPreKeySignature = bundle.findChild("signedPreKeySignature");
if(signedPreKeySignature == null) {
return null;
}
return Base64.decode(signedPreKeySignature.getContent(),Base64.DEFAULT);
}
public IdentityKey identityKey(final Element bundle) {
IdentityKey identityKey = null;
final Element identityKeyElement = bundle.findChild("identityKey");
if(identityKeyElement == null) {
return null;
}
try {
identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage());
}
return identityKey;
}
public Map<Integer, ECPublicKey> preKeyPublics(final IqPacket packet) {
Map<Integer, ECPublicKey> preKeyRecords = new HashMap<>();
Element item = getItem(packet);
if (item == null) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <item> in bundle IQ packet: " + packet);
return null;
}
final Element bundleElement = item.findChild("bundle");
if(bundleElement == null) {
return null;
}
final Element prekeysElement = bundleElement.findChild("prekeys");
if(prekeysElement == null) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Couldn't find <prekeys> in bundle IQ packet: " + packet);
return null;
}
for(Element preKeyPublicElement : prekeysElement.getChildren()) {
if(!preKeyPublicElement.getName().equals("preKeyPublic")){
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Encountered unexpected tag in prekeys list: " + preKeyPublicElement);
continue;
}
Integer preKeyId = Integer.valueOf(preKeyPublicElement.getAttribute("preKeyId"));
try {
ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0);
preKeyRecords.put(preKeyId, preKeyPublic);
} catch (InvalidKeyException e) {
Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping...");
continue;
}
}
return preKeyRecords;
}
public PreKeyBundle bundle(final IqPacket bundle) {
Element bundleItem = getItem(bundle);
if(bundleItem == null) {
return null;
}
final Element bundleElement = bundleItem.findChild("bundle");
if(bundleElement == null) {
return null;
}
ECPublicKey signedPreKeyPublic = signedPreKeyPublic(bundleElement);
Integer signedPreKeyId = signedPreKeyId(bundleElement);
byte[] signedPreKeySignature = signedPreKeySignature(bundleElement);
IdentityKey identityKey = identityKey(bundleElement);
if(signedPreKeyPublic == null || identityKey == null) {
return null;
}
return new PreKeyBundle(0, 0, 0, null,
signedPreKeyId, signedPreKeyPublic, signedPreKeySignature, identityKey);
}
public List<PreKeyBundle> preKeys(final IqPacket preKeys) {
List<PreKeyBundle> bundles = new ArrayList<>();
Map<Integer, ECPublicKey> preKeyPublics = preKeyPublics(preKeys);
if ( preKeyPublics != null) {
for (Integer preKeyId : preKeyPublics.keySet()) {
ECPublicKey preKeyPublic = preKeyPublics.get(preKeyId);
bundles.add(new PreKeyBundle(0, 0, preKeyId, preKeyPublic,
0, null, null, null));
}
}
return bundles;
}
@Override
public void onIqPacketReceived(final Account account, final IqPacket packet) {
if (packet.hasChild("query", Xmlns.ROSTER) && packet.fromServer(account)) {

View File

@ -6,7 +6,11 @@ import android.util.Pair;
import net.java.otr4j.session.Session;
import net.java.otr4j.session.SessionStatus;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -69,11 +73,9 @@ public class MessageParser extends AbstractParser implements
body = otrSession.transformReceiving(body);
SessionStatus status = otrSession.getSessionStatus();
if (body == null && status == SessionStatus.ENCRYPTED) {
conversation.setNextEncryption(Message.ENCRYPTION_OTR);
mXmppConnectionService.onOtrSessionEstablished(conversation);
return null;
} else if (body == null && status == SessionStatus.FINISHED) {
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
conversation.resetOtrSession();
mXmppConnectionService.updateConversationUi();
return null;
@ -94,6 +96,20 @@ public class MessageParser extends AbstractParser implements
}
}
private Message parseAxolotlChat(Element axolotlMessage, Jid from, String id, Conversation conversation, int status) {
Message finishedMessage = null;
AxolotlService service = conversation.getAccount().getAxolotlService();
XmppAxolotlMessage xmppAxolotlMessage = XmppAxolotlMessage.fromElement(axolotlMessage, from.toBareJid());
XmppAxolotlMessage.XmppAxolotlPlaintextMessage plaintextMessage = service.processReceivingPayloadMessage(xmppAxolotlMessage);
if(plaintextMessage != null) {
finishedMessage = new Message(conversation, plaintextMessage.getPlaintext(), Message.ENCRYPTION_AXOLOTL, status);
finishedMessage.setAxolotlFingerprint(plaintextMessage.getFingerprint());
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(finishedMessage.getConversation().getAccount())+" Received Message with session fingerprint: "+plaintextMessage.getFingerprint());
}
return finishedMessage;
}
private class Invite {
Jid jid;
String password;
@ -170,6 +186,13 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateAccountUi();
}
} else if (AxolotlService.PEP_DEVICE_LIST.equals(node)) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Received PEP device list update from "+ from + ", processing...");
Element item = items.findChild("item");
Set<Integer> deviceIds = mXmppConnectionService.getIqParser().deviceIds(item);
AxolotlService axolotlService = account.getAxolotlService();
axolotlService.registerDevices(from, deviceIds);
mXmppConnectionService.updateAccountUi();
}
}
@ -177,6 +200,13 @@ public class MessageParser extends AbstractParser implements
if (packet.getType() == MessagePacket.TYPE_ERROR) {
Jid from = packet.getFrom();
if (from != null) {
Element error = packet.findChild("error");
String text = error == null ? null : error.findChildContent("text");
if (text != null) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + text);
} else if (error != null) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending message to "+ from+ " failed - " + error);
}
Message message = mXmppConnectionService.markMessage(account,
from.toBareJid(),
packet.getId(),
@ -198,6 +228,7 @@ public class MessageParser extends AbstractParser implements
final MessagePacket packet;
Long timestamp = null;
final boolean isForwarded;
boolean isCarbon = false;
String serverMsgId = null;
final Element fin = original.findChild("fin", "urn:xmpp:mam:0");
if (fin != null) {
@ -228,7 +259,8 @@ public class MessageParser extends AbstractParser implements
return;
}
timestamp = f != null ? f.second : null;
isForwarded = f != null;
isCarbon = f != null;
isForwarded = isCarbon;
} else {
packet = original;
isForwarded = false;
@ -238,8 +270,9 @@ public class MessageParser extends AbstractParser implements
timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis());
}
final String body = packet.getBody();
final String encrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element mucUserElement = packet.findChild("x","http://jabber.org/protocol/muc#user");
final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user");
final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
int status;
final Jid counterpart;
final Jid to = packet.getTo();
@ -267,11 +300,11 @@ public class MessageParser extends AbstractParser implements
return;
}
if (extractChatState(mXmppConnectionService.find(account,from), packet)) {
if (extractChatState(mXmppConnectionService.find(account, from), packet)) {
mXmppConnectionService.updateConversationUi();
}
if ((body != null || encrypted != null) && !isMucStatusMessage) {
if ((body != null || pgpEncrypted != null || axolotlEncrypted != null) && !isMucStatusMessage) {
Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat);
if (isTypeGroupChat) {
if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) {
@ -300,14 +333,20 @@ public class MessageParser extends AbstractParser implements
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
} else if (encrypted != null) {
message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status);
} else if (pgpEncrypted != null) {
message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status);
} else if (axolotlEncrypted != null) {
message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status);
if (message == null) {
return;
}
} else {
message = new Message(conversation, body, Message.ENCRYPTION_NONE, status);
}
message.setCounterpart(counterpart);
message.setRemoteMsgId(remoteMsgId);
message.setServerMsgId(serverMsgId);
message.setCarbon(isCarbon);
message.setTime(timestamp);
message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0");
if (conversation.getMode() == Conversation.MODE_MULTI) {
@ -338,15 +377,19 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.updateConversationUi();
}
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded) {
if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) {
if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) {
MessagePacket receipt = mXmppConnectionService
.getMessageGenerator().received(account, packet, "urn:xmpp:chat-markers:0");
MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
packet,
"urn:xmpp:chat-markers:0",
MessagePacket.TYPE_CHAT);
mXmppConnectionService.sendMessagePacket(account, receipt);
}
if (packet.hasChild("request", "urn:xmpp:receipts")) {
MessagePacket receipt = mXmppConnectionService
.getMessageGenerator().received(account, packet, "urn:xmpp:receipts");
MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account,
packet,
"urn:xmpp:receipts",
packet.getType());
mXmppConnectionService.sendMessagePacket(account, receipt);
}
}

View File

@ -1,10 +1,34 @@
package eu.siacs.conversations.persistance;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Base64;
import android.util.Log;
import org.whispersystems.libaxolotl.AxolotlAddress;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.state.PreKeyRecord;
import org.whispersystems.libaxolotl.state.SessionRecord;
import org.whispersystems.libaxolotl.state.SignedPreKeyRecord;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.SQLiteAxolotlStore;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -13,19 +37,12 @@ import eu.siacs.conversations.entities.Roster;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
public class DatabaseBackend extends SQLiteOpenHelper {
private static DatabaseBackend instance = null;
private static final String DATABASE_NAME = "history";
private static final int DATABASE_VERSION = 14;
private static final int DATABASE_VERSION = 16;
private static String CREATE_CONTATCS_STATEMENT = "create table "
+ Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, "
@ -39,6 +56,60 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", "
+ Contact.JID + ") ON CONFLICT REPLACE);";
private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE "
+ SQLiteAxolotlStore.PREKEY_TABLENAME + "("
+ SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ SQLiteAxolotlStore.ID + " INTEGER, "
+ SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ SQLiteAxolotlStore.ID
+ ") ON CONFLICT REPLACE"
+");";
private static String CREATE_SIGNED_PREKEYS_STATEMENT = "CREATE TABLE "
+ SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME + "("
+ SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ SQLiteAxolotlStore.ID + " INTEGER, "
+ SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ SQLiteAxolotlStore.ID
+ ") ON CONFLICT REPLACE"+
");";
private static String CREATE_SESSIONS_STATEMENT = "CREATE TABLE "
+ SQLiteAxolotlStore.SESSION_TABLENAME + "("
+ SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ SQLiteAxolotlStore.NAME + " TEXT, "
+ SQLiteAxolotlStore.DEVICE_ID + " INTEGER, "
+ SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ SQLiteAxolotlStore.NAME + ", "
+ SQLiteAxolotlStore.DEVICE_ID
+ ") ON CONFLICT REPLACE"
+");";
private static String CREATE_IDENTITIES_STATEMENT = "CREATE TABLE "
+ SQLiteAxolotlStore.IDENTITIES_TABLENAME + "("
+ SQLiteAxolotlStore.ACCOUNT + " TEXT, "
+ SQLiteAxolotlStore.NAME + " TEXT, "
+ SQLiteAxolotlStore.OWN + " INTEGER, "
+ SQLiteAxolotlStore.FINGERPRINT + " TEXT, "
+ SQLiteAxolotlStore.TRUSTED + " INTEGER, "
+ SQLiteAxolotlStore.KEY + " TEXT, FOREIGN KEY("
+ SQLiteAxolotlStore.ACCOUNT
+ ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID + ") ON DELETE CASCADE, "
+ "UNIQUE( " + SQLiteAxolotlStore.ACCOUNT + ", "
+ SQLiteAxolotlStore.NAME + ", "
+ SQLiteAxolotlStore.FINGERPRINT
+ ") ON CONFLICT IGNORE"
+");";
private DatabaseBackend(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@ -69,12 +140,18 @@ public class DatabaseBackend extends SQLiteOpenHelper {
+ Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, "
+ Message.RELATIVE_FILE_PATH + " TEXT, "
+ Message.SERVER_MSG_ID + " TEXT, "
+ Message.FINGERPRINT + " TEXT, "
+ Message.CARBON + " INTEGER, "
+ Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY("
+ Message.CONVERSATION + ") REFERENCES "
+ Conversation.TABLENAME + "(" + Conversation.UUID
+ ") ON DELETE CASCADE);");
db.execSQL(CREATE_CONTATCS_STATEMENT);
db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
}
@Override
@ -215,6 +292,15 @@ public class DatabaseBackend extends SQLiteOpenHelper {
}
cursor.close();
}
if (oldVersion < 15 && newVersion >= 15) {
recreateAxolotlDb(db);
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.FINGERPRINT + " TEXT");
}
if (oldVersion < 16 && newVersion >= 16) {
db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN "
+ Message.CARBON + " INTEGER");
}
}
public static synchronized DatabaseBackend getInstance(Context context) {
@ -311,7 +397,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
};
Cursor cursor = db.query(Conversation.TABLENAME, null,
Conversation.ACCOUNT + "=? AND (" + Conversation.CONTACTJID
+ " like ? OR "+Conversation.CONTACTJID+"=?)", selectionArgs, null, null, null);
+ " like ? OR " + Conversation.CONTACTJID + "=?)", selectionArgs, null, null, null);
if (cursor.getCount() == 0)
return null;
cursor.moveToFirst();
@ -481,4 +567,405 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.close();
return list;
}
private Cursor getCursorForSession(Account account, AxolotlAddress contact) {
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = null;
String[] selectionArgs = {account.getUuid(),
contact.getName(),
Integer.toString(contact.getDeviceId())};
Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
columns,
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.NAME + " = ? AND "
+ SQLiteAxolotlStore.DEVICE_ID + " = ? ",
selectionArgs,
null, null, null);
return cursor;
}
public SessionRecord loadSession(Account account, AxolotlAddress contact) {
SessionRecord session = null;
Cursor cursor = getCursorForSession(account, contact);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
session = new SessionRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e) {
cursor.close();
throw new AssertionError(e);
}
}
cursor.close();
return session;
}
public List<Integer> getSubDeviceSessions(Account account, AxolotlAddress contact) {
List<Integer> devices = new ArrayList<>();
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {SQLiteAxolotlStore.DEVICE_ID};
String[] selectionArgs = {account.getUuid(),
contact.getName()};
Cursor cursor = db.query(SQLiteAxolotlStore.SESSION_TABLENAME,
columns,
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.NAME + " = ?",
selectionArgs,
null, null, null);
while(cursor.moveToNext()) {
devices.add(cursor.getInt(
cursor.getColumnIndex(SQLiteAxolotlStore.DEVICE_ID)));
}
cursor.close();
return devices;
}
public boolean containsSession(Account account, AxolotlAddress contact) {
Cursor cursor = getCursorForSession(account, contact);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storeSession(Account account, AxolotlAddress contact, SessionRecord session) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.NAME, contact.getName());
values.put(SQLiteAxolotlStore.DEVICE_ID, contact.getDeviceId());
values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(session.serialize(),Base64.DEFAULT));
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(SQLiteAxolotlStore.SESSION_TABLENAME, null, values);
}
public void deleteSession(Account account, AxolotlAddress contact) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(),
contact.getName(),
Integer.toString(contact.getDeviceId())};
db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.NAME + " = ? AND "
+ SQLiteAxolotlStore.DEVICE_ID + " = ? ",
args);
}
public void deleteAllSessions(Account account, AxolotlAddress contact) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), contact.getName()};
db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ SQLiteAxolotlStore.NAME + " = ?",
args);
}
private Cursor getCursorForPreKey(Account account, int preKeyId) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid(), Integer.toString(preKeyId)};
Cursor cursor = db.query(SQLiteAxolotlStore.PREKEY_TABLENAME,
columns,
SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ SQLiteAxolotlStore.ID + "=?",
selectionArgs,
null, null, null);
return cursor;
}
public PreKeyRecord loadPreKey(Account account, int preKeyId) {
PreKeyRecord record = null;
Cursor cursor = getCursorForPreKey(account, preKeyId);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
record = new PreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e ) {
throw new AssertionError(e);
}
}
cursor.close();
return record;
}
public boolean containsPreKey(Account account, int preKeyId) {
Cursor cursor = getCursorForPreKey(account, preKeyId);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storePreKey(Account account, PreKeyRecord record) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.ID, record.getId());
values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(SQLiteAxolotlStore.PREKEY_TABLENAME, null, values);
}
public void deletePreKey(Account account, int preKeyId) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), Integer.toString(preKeyId)};
db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ SQLiteAxolotlStore.ID + "=?",
args);
}
private Cursor getCursorForSignedPreKey(Account account, int signedPreKeyId) {
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid(), Integer.toString(signedPreKeyId)};
Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
columns,
SQLiteAxolotlStore.ACCOUNT + "=? AND " + SQLiteAxolotlStore.ID + "=?",
selectionArgs,
null, null, null);
return cursor;
}
public SignedPreKeyRecord loadSignedPreKey(Account account, int signedPreKeyId) {
SignedPreKeyRecord record = null;
Cursor cursor = getCursorForSignedPreKey(account, signedPreKeyId);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
record = new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (IOException e ) {
throw new AssertionError(e);
}
}
cursor.close();
return record;
}
public List<SignedPreKeyRecord> loadSignedPreKeys(Account account) {
List<SignedPreKeyRecord> prekeys = new ArrayList<>();
SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {SQLiteAxolotlStore.KEY};
String[] selectionArgs = {account.getUuid()};
Cursor cursor = db.query(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
columns,
SQLiteAxolotlStore.ACCOUNT + "=?",
selectionArgs,
null, null, null);
while(cursor.moveToNext()) {
try {
prekeys.add(new SignedPreKeyRecord(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)), Base64.DEFAULT)));
} catch (IOException ignored) {
}
}
cursor.close();
return prekeys;
}
public boolean containsSignedPreKey(Account account, int signedPreKeyId) {
Cursor cursor = getCursorForPreKey(account, signedPreKeyId);
int count = cursor.getCount();
cursor.close();
return count != 0;
}
public void storeSignedPreKey(Account account, SignedPreKeyRecord record) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.ID, record.getId());
values.put(SQLiteAxolotlStore.KEY, Base64.encodeToString(record.serialize(),Base64.DEFAULT));
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
db.insert(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME, null, values);
}
public void deleteSignedPreKey(Account account, int signedPreKeyId) {
SQLiteDatabase db = this.getWritableDatabase();
String[] args = {account.getUuid(), Integer.toString(signedPreKeyId)};
db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + "=? AND "
+ SQLiteAxolotlStore.ID + "=?",
args);
}
private Cursor getIdentityKeyCursor(Account account, String name, boolean own) {
return getIdentityKeyCursor(account, name, own, null);
}
private Cursor getIdentityKeyCursor(Account account, String fingerprint) {
return getIdentityKeyCursor(account, null, null, fingerprint);
}
private Cursor getIdentityKeyCursor(Account account, String name, Boolean own, String fingerprint) {
final SQLiteDatabase db = this.getReadableDatabase();
String[] columns = {SQLiteAxolotlStore.TRUSTED,
SQLiteAxolotlStore.KEY};
ArrayList<String> selectionArgs = new ArrayList<>(4);
selectionArgs.add(account.getUuid());
String selectionString = SQLiteAxolotlStore.ACCOUNT + " = ?";
if (name != null){
selectionArgs.add(name);
selectionString += " AND " + SQLiteAxolotlStore.NAME + " = ?";
}
if (fingerprint != null){
selectionArgs.add(fingerprint);
selectionString += " AND " + SQLiteAxolotlStore.FINGERPRINT + " = ?";
}
if (own != null){
selectionArgs.add(own?"1":"0");
selectionString += " AND " + SQLiteAxolotlStore.OWN + " = ?";
}
Cursor cursor = db.query(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
columns,
selectionString,
selectionArgs.toArray(new String[selectionArgs.size()]),
null, null, null);
return cursor;
}
public IdentityKeyPair loadOwnIdentityKeyPair(Account account, String name) {
IdentityKeyPair identityKeyPair = null;
Cursor cursor = getIdentityKeyCursor(account, name, true);
if(cursor.getCount() != 0) {
cursor.moveToFirst();
try {
identityKeyPair = new IdentityKeyPair(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT));
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account" + account.getJid().toBareJid() + ", address: " + name);
}
}
cursor.close();
return identityKeyPair;
}
public Set<IdentityKey> loadIdentityKeys(Account account, String name) {
return loadIdentityKeys(account, name, null);
}
public Set<IdentityKey> loadIdentityKeys(Account account, String name, XmppAxolotlSession.Trust trust) {
Set<IdentityKey> identityKeys = new HashSet<>();
Cursor cursor = getIdentityKeyCursor(account, name, false);
while(cursor.moveToNext()) {
if ( trust != null &&
cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED))
!= trust.getCode()) {
continue;
}
try {
identityKeys.add(new IdentityKey(Base64.decode(cursor.getString(cursor.getColumnIndex(SQLiteAxolotlStore.KEY)),Base64.DEFAULT),0));
} catch (InvalidKeyException e) {
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+"Encountered invalid IdentityKey in database for account"+account.getJid().toBareJid()+", address: "+name);
}
}
cursor.close();
return identityKeys;
}
public long numTrustedKeys(Account account, String name) {
SQLiteDatabase db = getReadableDatabase();
String[] args = {
account.getUuid(),
name,
String.valueOf(XmppAxolotlSession.Trust.TRUSTED.getCode())
};
return DatabaseUtils.queryNumEntries(db, SQLiteAxolotlStore.IDENTITIES_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ?"
+ " AND " + SQLiteAxolotlStore.NAME + " = ?"
+ " AND " + SQLiteAxolotlStore.TRUSTED + " = ?",
args
);
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized) {
storeIdentityKey(account, name, own, fingerprint, base64Serialized, XmppAxolotlSession.Trust.UNDECIDED);
}
private void storeIdentityKey(Account account, String name, boolean own, String fingerprint, String base64Serialized, XmppAxolotlSession.Trust trusted) {
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.ACCOUNT, account.getUuid());
values.put(SQLiteAxolotlStore.NAME, name);
values.put(SQLiteAxolotlStore.OWN, own ? 1 : 0);
values.put(SQLiteAxolotlStore.FINGERPRINT, fingerprint);
values.put(SQLiteAxolotlStore.KEY, base64Serialized);
values.put(SQLiteAxolotlStore.TRUSTED, trusted.getCode());
db.insert(SQLiteAxolotlStore.IDENTITIES_TABLENAME, null, values);
}
public XmppAxolotlSession.Trust isIdentityKeyTrusted(Account account, String fingerprint) {
Cursor cursor = getIdentityKeyCursor(account, fingerprint);
XmppAxolotlSession.Trust trust = null;
if (cursor.getCount() > 0) {
cursor.moveToFirst();
int trustValue = cursor.getInt(cursor.getColumnIndex(SQLiteAxolotlStore.TRUSTED));
trust = XmppAxolotlSession.Trust.fromCode(trustValue);
}
cursor.close();
return trust;
}
public boolean setIdentityKeyTrust(Account account, String fingerprint, XmppAxolotlSession.Trust trust) {
SQLiteDatabase db = this.getWritableDatabase();
String[] selectionArgs = {
account.getUuid(),
fingerprint
};
ContentValues values = new ContentValues();
values.put(SQLiteAxolotlStore.TRUSTED, trust.getCode());
int rows = db.update(SQLiteAxolotlStore.IDENTITIES_TABLENAME, values,
SQLiteAxolotlStore.ACCOUNT + " = ? AND "
+ SQLiteAxolotlStore.FINGERPRINT + " = ? ",
selectionArgs);
return rows == 1;
}
public void storeIdentityKey(Account account, String name, IdentityKey identityKey) {
storeIdentityKey(account, name, false, identityKey.getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKey.serialize(), Base64.DEFAULT));
}
public void storeOwnIdentityKeyPair(Account account, String name, IdentityKeyPair identityKeyPair) {
storeIdentityKey(account, name, true, identityKeyPair.getPublicKey().getFingerprint().replaceAll("\\s", ""), Base64.encodeToString(identityKeyPair.serialize(), Base64.DEFAULT), XmppAxolotlSession.Trust.TRUSTED);
}
public void recreateAxolotlDb() {
recreateAxolotlDb(getWritableDatabase());
}
public void recreateAxolotlDb(SQLiteDatabase db) {
Log.d(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+">>> (RE)CREATING AXOLOTL DATABASE <<<");
db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SESSION_TABLENAME);
db.execSQL(CREATE_SESSIONS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.PREKEY_TABLENAME);
db.execSQL(CREATE_PREKEYS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME);
db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT);
db.execSQL("DROP TABLE IF EXISTS " + SQLiteAxolotlStore.IDENTITIES_TABLENAME);
db.execSQL(CREATE_IDENTITIES_STATEMENT);
}
public void wipeAxolotlDb(Account account) {
String accountName = account.getUuid();
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account)+">>> WIPING AXOLOTL DATABASE FOR ACCOUNT " + accountName + " <<<");
SQLiteDatabase db = this.getWritableDatabase();
String[] deleteArgs= {
accountName
};
db.delete(SQLiteAxolotlStore.SESSION_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(SQLiteAxolotlStore.PREKEY_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(SQLiteAxolotlStore.SIGNED_PREKEY_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
db.delete(SQLiteAxolotlStore.IDENTITIES_TABLENAME,
SQLiteAxolotlStore.ACCOUNT + " = ?",
deleteArgs);
}
}

View File

@ -1,22 +1,5 @@
package eu.siacs.conversations.persistance;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
@ -31,14 +14,33 @@ import android.util.Base64OutputStream;
import android.util.Log;
import android.webkit.MimeTypeMap;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.URL;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Locale;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.utils.FileUtils;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend {
@ -126,25 +128,25 @@ public class FileBackend {
return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
}
public String getOriginalPath(Uri uri) {
String path = null;
if (uri.getScheme().equals("file")) {
return uri.getPath();
} else if (uri.toString().startsWith("content://media/")) {
String[] projection = {MediaStore.MediaColumns.DATA};
Cursor metaCursor = mXmppConnectionService.getContentResolver().query(uri,
projection, null, null, null);
if (metaCursor != null) {
public boolean useImageAsIs(Uri uri) {
String path = getOriginalPath(uri);
if (path == null) {
return false;
}
Log.d(Config.LOGTAG,"using image as is. path: "+path);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
try {
if (metaCursor.moveToFirst()) {
path = metaCursor.getString(0);
}
} finally {
metaCursor.close();
BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver().openInputStream(uri), null, options);
return (options.outWidth <= Config.IMAGE_SIZE && options.outHeight <= Config.IMAGE_SIZE && options.outMimeType.contains(Config.IMAGE_FORMAT.name().toLowerCase()));
} catch (FileNotFoundException e) {
return false;
}
}
}
return path;
public String getOriginalPath(Uri uri) {
Log.d(Config.LOGTAG,"get original path for uri: "+uri.toString());
return FileUtils.getPath(mXmppConnectionService,uri);
}
public DownloadableFile copyFileToPrivateStorage(Message message, Uri uri) throws FileCopyException {
@ -184,8 +186,18 @@ public class FileBackend {
return this.copyImageToPrivateStorage(message, image, 0);
}
private DownloadableFile copyImageToPrivateStorage(Message message,
Uri image, int sampleSize) throws FileCopyException {
private DownloadableFile copyImageToPrivateStorage(Message message,Uri image, int sampleSize) throws FileCopyException {
switch(Config.IMAGE_FORMAT) {
case JPEG:
message.setRelativeFilePath(message.getUuid()+".jpg");
break;
case PNG:
message.setRelativeFilePath(message.getUuid()+".png");
break;
case WEBP:
message.setRelativeFilePath(message.getUuid()+".webp");
break;
}
DownloadableFile file = getFile(message);
file.getParentFile().mkdirs();
InputStream is = null;
@ -205,13 +217,13 @@ public class FileBackend {
if (originalBitmap == null) {
throw new FileCopyException(R.string.error_not_an_image_file);
}
Bitmap scaledBitmap = resize(originalBitmap, IMAGE_SIZE);
Bitmap scaledBitmap = resize(originalBitmap, Config.IMAGE_SIZE);
int rotation = getRotation(image);
if (rotation > 0) {
scaledBitmap = rotate(scaledBitmap, rotation);
}
boolean success = scaledBitmap.compress(Bitmap.CompressFormat.WEBP, 75, os);
boolean success = scaledBitmap.compress(Config.IMAGE_FORMAT, Config.IMAGE_QUALITY, os);
if (!success) {
throw new FileCopyException(R.string.error_compressing_image);
}
@ -546,4 +558,13 @@ public class FileBackend {
}
}
}
public static void close(Socket socket) {
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
}
}
}
}

View File

@ -1,5 +1,35 @@
package eu.siacs.conversations.services;
import android.content.Context;
import android.os.PowerManager;
import android.util.Log;
import android.util.Pair;
import org.bouncycastle.crypto.engines.AESEngine;
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.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
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 eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.DownloadableFile;
public class AbstractConnectionManager {
protected XmppConnectionService mXmppConnectionService;
@ -20,4 +50,75 @@ public class AbstractConnectionManager {
return 524288;
}
}
public static Pair<InputStream,Integer> createInputStream(DownloadableFile file, boolean gcm) throws FileNotFoundException {
FileInputStream is;
int size;
is = new FileInputStream(file);
size = (int) file.getSize();
if (file.getKey() == null) {
return new Pair<InputStream,Integer>(is,size);
}
try {
if (gcm) {
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
InputStream cis = new org.bouncycastle.crypto.io.CipherInputStream(is, cipher);
return new Pair<>(cis, cipher.getOutputSize(size));
} else {
IvParameterSpec ips = new IvParameterSpec(file.getIv());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
Log.d(Config.LOGTAG, "opening encrypted input stream");
final int s = Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE ? size : (size / 16 + 1) * 16;
return new Pair<InputStream,Integer>(new CipherInputStream(is, cipher),s);
}
} catch (InvalidKeyException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (NoSuchPaddingException e) {
return null;
} catch (InvalidAlgorithmParameterException e) {
return null;
}
}
public static OutputStream createOutputStream(DownloadableFile file, boolean gcm) {
FileOutputStream os;
try {
os = new FileOutputStream(file);
if (file.getKey() == null) {
return os;
}
} catch (FileNotFoundException e) {
return null;
}
try {
if (gcm) {
AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
return new org.bouncycastle.crypto.io.CipherOutputStream(os, cipher);
} else {
IvParameterSpec ips = new IvParameterSpec(file.getIv());
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(file.getKey(), "AES"), ips);
Log.d(Config.LOGTAG, "opening encrypted output stream");
return new CipherOutputStream(os, cipher);
}
} catch (InvalidKeyException e) {
return null;
} catch (NoSuchAlgorithmException e) {
return null;
} catch (NoSuchPaddingException e) {
return null;
} catch (InvalidAlgorithmParameterException e) {
return null;
}
}
public PowerManager.WakeLock createWakeLock(String name) {
PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE);
return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name);
}
}

View File

@ -1,10 +1,11 @@
package eu.siacs.conversations.services;
import eu.siacs.conversations.persistance.DatabaseBackend;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import eu.siacs.conversations.persistance.DatabaseBackend;
public class EventReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {

View File

@ -64,7 +64,7 @@ public class NotificationService {
return (message.getStatus() == Message.STATUS_RECEIVED)
&& notificationsEnabled()
&& !message.getConversation().isMuted()
&& (message.getConversation().getMode() == Conversation.MODE_SINGLE
&& (message.getConversation().isPnNA()
|| conferenceNotificationsEnabled()
|| wasHighlightedOrPrivate(message)
);
@ -332,9 +332,10 @@ public class NotificationService {
private Message getImage(final Iterable<Message> messages) {
for (final Message message : messages) {
if (message.getType() == Message.TYPE_IMAGE
if (message.getType() != Message.TYPE_TEXT
&& message.getTransferable() == null
&& message.getEncryption() != Message.ENCRYPTION_PGP) {
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& message.getFileParams().height > 0) {
return message;
}
}

View File

@ -52,16 +52,17 @@ import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
@ -85,6 +86,7 @@ import eu.siacs.conversations.xmpp.OnContactStatusChanged;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.OnMessageAcknowledged;
import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
import eu.siacs.conversations.xmpp.OnStatusChanged;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
@ -273,11 +275,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
syncDirtyContacts(account);
scheduleWakeUpCall(Config.PING_MAX_INTERVAL,account.getUuid().hashCode());
account.getAxolotlService().publishOwnDeviceIdIfNeeded();
account.getAxolotlService().publishBundlesIfNeeded();
scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode());
} else if (account.getStatus() == Account.State.OFFLINE) {
resetSendingToWaiting(account);
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
int timeToReconnect = mRandom.nextInt(50) + 10;
int timeToReconnect = mRandom.nextInt(20) + 10;
scheduleWakeUpCall(timeToReconnect,account.getUuid().hashCode());
}
} else if (account.getStatus() == Account.State.REGISTRATION_SUCCESSFUL) {
@ -304,6 +308,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
private int rosterChangedListenerCount = 0;
private OnMucRosterUpdate mOnMucRosterUpdate = null;
private int mucRosterChangedListenerCount = 0;
private OnKeyStatusUpdated mOnKeyStatusUpdated = null;
private int keyStatusUpdatedListenerCount = 0;
private SecureRandom mRandom;
private OpenPgpServiceConnection pgpServiceConnection;
private PgpEngine mPgpEngine = null;
@ -342,7 +348,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void attachLocationToConversation(final Conversation conversation,
final Uri uri,
final UiCallback<Message> callback) {
int encryption = conversation.getNextEncryption(forceEncryption());
int encryption = conversation.getNextEncryption();
if (encryption == Message.ENCRYPTION_PGP) {
encryption = Message.ENCRYPTION_DECRYPTED;
}
@ -361,12 +367,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
final Uri uri,
final UiCallback<Message> callback) {
final Message message;
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "",
Message.ENCRYPTION_DECRYPTED);
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
} else {
message = new Message(conversation, "",
conversation.getNextEncryption(forceEncryption()));
message = new Message(conversation, "", conversation.getNextEncryption());
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_FILE);
@ -399,15 +403,17 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
public void attachImageToConversation(final Conversation conversation,
final Uri uri, final UiCallback<Message> callback) {
public void attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) {
if (getFileBackend().useImageAsIs(uri)) {
Log.d(Config.LOGTAG,"using image as is");
attachFileToConversation(conversation, uri, callback);
return;
}
final Message message;
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "",
Message.ENCRYPTION_DECRYPTED);
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
message = new Message(conversation, "", Message.ENCRYPTION_DECRYPTED);
} else {
message = new Message(conversation, "",
conversation.getNextEncryption(forceEncryption()));
message = new Message(conversation, "",conversation.getNextEncryption());
}
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_IMAGE);
@ -417,7 +423,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void run() {
try {
getFileBackend().copyImageToPrivateStorage(message, uri);
if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) {
if (conversation.getNextEncryption() == Message.ENCRYPTION_PGP) {
getPgpEngine().encrypt(message, callback);
} else {
callback.success(message);
@ -591,9 +597,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
for (final Account account : this.accounts) {
account.initAccountServices(this);
}
restoreFromDatabase();
getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
@ -674,22 +677,22 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
private void sendFileMessage(final Message message) {
private void sendFileMessage(final Message message, final boolean delay) {
Log.d(Config.LOGTAG, "send file message");
final Account account = message.getConversation().getAccount();
final XmppConnection connection = account.getXmppConnection();
if (connection != null && connection.getFeatures().httpUpload()) {
mHttpConnectionManager.createNewUploadConnection(message);
mHttpConnectionManager.createNewUploadConnection(message, delay);
} else {
mJingleConnectionManager.createNewConnection(message);
}
}
public void sendMessage(final Message message) {
sendMessage(message, false);
sendMessage(message, false, false);
}
private void sendMessage(final Message message, final boolean resend) {
private void sendMessage(final Message message, final boolean resend, final boolean delay) {
final Account account = message.getConversation().getAccount();
final Conversation conversation = message.getConversation();
account.deactivateGracePeriod();
@ -699,7 +702,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (!resend && message.getEncryption() != Message.ENCRYPTION_OTR) {
message.getConversation().endOtrIfNeeded();
message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
message.getConversation().findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR,
new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
markMessage(message,Message.STATUS_SEND_FAILED);
@ -712,24 +716,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
case Message.ENCRYPTION_NONE:
if (message.needsUploading()) {
if (account.httpUploadAvailable() || message.fixCounterpart()) {
this.sendFileMessage(message);
this.sendFileMessage(message,delay);
} else {
break;
}
} else {
packet = mMessageGenerator.generateChat(message,resend);
packet = mMessageGenerator.generateChat(message);
}
break;
case Message.ENCRYPTION_PGP:
case Message.ENCRYPTION_DECRYPTED:
if (message.needsUploading()) {
if (account.httpUploadAvailable() || message.fixCounterpart()) {
this.sendFileMessage(message);
this.sendFileMessage(message,delay);
} else {
break;
}
} else {
packet = mMessageGenerator.generatePgpChat(message,resend);
packet = mMessageGenerator.generatePgpChat(message);
}
break;
case Message.ENCRYPTION_OTR:
@ -743,7 +747,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (message.needsUploading()) {
mJingleConnectionManager.createNewConnection(message);
} else {
packet = mMessageGenerator.generateOtrChat(message,resend);
packet = mMessageGenerator.generateOtrChat(message);
}
} else if (otrSession == null) {
if (message.fixCounterpart()) {
@ -753,6 +757,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
if (message.needsUploading()) {
if (account.httpUploadAvailable() || message.fixCounterpart()) {
this.sendFileMessage(message,delay);
} else {
break;
}
} else {
XmppAxolotlMessage axolotlMessage = account.getAxolotlService().fetchAxolotlMessageFromCache(message);
if (axolotlMessage == null) {
account.getAxolotlService().preparePayloadMessage(message, delay);
} else {
packet = mMessageGenerator.generateAxolotlChat(message, axolotlMessage);
}
}
break;
}
if (packet != null) {
if (account.getXmppConnection().getFeatures().sm() || conversation.getMode() == Conversation.MODE_MULTI) {
@ -780,6 +802,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
conversation.startOtrSession(message.getCounterpart().getResourcepart(), false);
}
break;
case Message.ENCRYPTION_AXOLOTL:
message.setAxolotlFingerprint(account.getAxolotlService().getOwnPublicKey().getFingerprint().replaceAll("\\s", ""));
break;
}
}
@ -799,6 +824,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
updateConversationUi();
}
if (packet != null) {
if (delay) {
mMessageGenerator.addDelay(packet,message.getTimeSent());
}
if (conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) {
if (this.sendChatStates()) {
packet.addChild(ChatState.toElement(conversation.getOutgoingChatState()));
@ -813,13 +841,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
@Override
public void onMessageFound(Message message) {
resendMessage(message);
resendMessage(message, true);
}
});
}
public void resendMessage(final Message message) {
sendMessage(message, true);
public void resendMessage(final Message message, final boolean delay) {
sendMessage(message, true, delay);
}
public void fetchRosterFromServer(final Account account) {
@ -830,7 +858,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
} else {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster");
}
iqPacket.query(Xmlns.ROSTER).setAttribute("ver",account.getRosterVersion());
iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion());
sendIqPacket(account,iqPacket,mIqParser);
}
@ -943,6 +971,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
Log.d(Config.LOGTAG,"restoring roster");
for(Account account : accounts) {
databaseBackend.readRoster(account.getRoster());
account.initAccountServices(XmppConnectionService.this);
}
getBitmapCache().evictAll();
Looper.prepare();
@ -974,6 +1003,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
public void onMessageFound(Message message) {
if (!getFileBackend().isFileAvailable(message)) {
message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
final int s = message.getStatus();
if(s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
markMessage(message,Message.STATUS_SEND_FAILED);
}
}
}
});
@ -985,8 +1018,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (message != null) {
if (!getFileBackend().isFileAvailable(message)) {
message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_DELETED));
final int s = message.getStatus();
if(s == Message.STATUS_WAITING || s == Message.STATUS_OFFERED || s == Message.STATUS_UNSEND) {
markMessage(message,Message.STATUS_SEND_FAILED);
} else {
updateConversationUi();
}
}
return;
}
}
@ -1342,6 +1380,31 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
public void setOnKeyStatusUpdatedListener(final OnKeyStatusUpdated listener) {
synchronized (this) {
if (checkListeners()) {
switchToForeground();
}
this.mOnKeyStatusUpdated = listener;
if (this.keyStatusUpdatedListenerCount < 2) {
this.keyStatusUpdatedListenerCount++;
}
}
}
public void removeOnNewKeysAvailableListener() {
synchronized (this) {
this.keyStatusUpdatedListenerCount--;
if (this.keyStatusUpdatedListenerCount <= 0) {
this.keyStatusUpdatedListenerCount = 0;
this.mOnKeyStatusUpdated = null;
if (checkListeners()) {
switchToBackground();
}
}
}
}
public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) {
synchronized (this) {
if (checkListeners()) {
@ -1372,7 +1435,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
&& this.mOnConversationUpdate == null
&& this.mOnRosterUpdate == null
&& this.mOnUpdateBlocklist == null
&& this.mOnShowErrorToast == null);
&& this.mOnShowErrorToast == null
&& this.mOnKeyStatusUpdated == null);
}
private void switchToForeground() {
@ -1784,7 +1848,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
account.getJid().toBareJid() + " otr session established with "
+ conversation.getJid() + "/"
+ otrSession.getSessionID().getUserID());
conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() {
conversation.findUnsentMessagesWithEncryption(Message.ENCRYPTION_OTR, new Conversation.OnMessageFound() {
@Override
public void onMessageFound(Message message) {
@ -1797,8 +1861,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
if (message.needsUploading()) {
mJingleConnectionManager.createNewConnection(message);
} else {
MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true);
MessagePacket outPacket = mMessageGenerator.generateOtrChat(message);
if (outPacket != null) {
mMessageGenerator.addDelay(outPacket, message.getTimeSent());
message.setStatus(Message.STATUS_SEND);
databaseBackend.updateMessage(message);
sendMessagePacket(account, outPacket);
@ -2260,6 +2325,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
public void keyStatusUpdated() {
if(mOnKeyStatusUpdated != null) {
mOnKeyStatusUpdated.onKeyStatusUpdated();
}
}
public Account findAccountByJid(final Jid accountJid) {
for (Account account : this.accounts) {
if (account.getJid().toBareJid().equals(accountJid.toBareJid())) {
@ -2470,8 +2541,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa
}
}
for (final Message msg : messages) {
msg.setTime(System.currentTimeMillis());
markMessage(msg, Message.STATUS_WAITING);
this.resendMessage(msg);
this.resendMessage(msg,false);
}
}

View File

@ -2,7 +2,6 @@ package eu.siacs.conversations.ui;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.preference.Preference;
import android.util.AttributeSet;

View File

@ -55,16 +55,10 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
}
Collections.sort(getListItems());
}
runOnUiThread(new Runnable() {
@Override
public void run() {
getListItemAdapter().notifyDataSetChanged();
}
});
}
@Override
public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) {
protected void refreshUiReal() {
final Editable editable = getSearchEditText().getText();
if (editable != null) {
filterContacts(editable.toString());
@ -72,4 +66,9 @@ public class BlocklistActivity extends AbstractSearchableListItemActivity implem
filterContacts();
}
}
@Override
public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) {
refreshUi();
}
}

View File

@ -4,7 +4,6 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import eu.siacs.conversations.R;
@ -104,4 +103,8 @@ public class ChangePasswordActivity extends XmppActivity implements XmppConnecti
});
}
public void refreshUiReal() {
}
}

View File

@ -13,11 +13,11 @@ import android.widget.AbsListView.MultiChoiceModeListener;
import android.widget.AdapterView;
import android.widget.ListView;
import java.util.Set;
import java.util.HashSet;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
@ -149,4 +149,8 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity {
return result.toArray(new String[result.size()]);
}
public void refreshUiReal() {
//nothing to do. This Activity doesn't implement any listeners
}
}

View File

@ -27,8 +27,8 @@ import org.openintents.openpgp.util.OpenPgpUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.entities.Account;
@ -38,8 +38,8 @@ import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnMucRosterUpdate;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ConferenceDetailsActivity extends XmppActivity implements OnConversationUpdate, OnMucRosterUpdate, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoleChanged, XmppConnectionService.OnConferenceOptionsPushed {
@ -266,14 +266,17 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
final User self = mConversation.getMucOptions().getSelf();
this.mSelectedUser = user;
String name;
if (user.getJid() != null) {
final Contact contact = user.getContact();
if (contact != null) {
name = contact.getDisplayName();
} else {
} else if (user.getJid() != null){
name = user.getJid().toBareJid().toString();
} else {
name = user.getName();
}
menu.setHeaderTitle(name);
if (user.getJid() != null) {
MenuItem showContactDetails = menu.findItem(R.id.action_contact_details);
MenuItem startConversation = menu.findItem(R.id.start_conversation);
MenuItem giveMembership = menu.findItem(R.id.give_membership);
MenuItem removeMembership = menu.findItem(R.id.remove_membership);
@ -282,6 +285,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
MenuItem removeFromRoom = menu.findItem(R.id.remove_from_room);
MenuItem banFromConference = menu.findItem(R.id.ban_from_conference);
startConversation.setVisible(true);
if (contact != null) {
showContactDetails.setVisible(true);
}
if (self.getAffiliation().ranks(MucOptions.Affiliation.ADMIN) &&
self.getAffiliation().outranks(user.getAffiliation())) {
if (mAdvancedMode) {
@ -300,15 +306,24 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
removeAdminPrivileges.setVisible(true);
}
}
} else {
MenuItem sendPrivateMessage = menu.findItem(R.id.send_private_message);
sendPrivateMessage.setVisible(true);
}
}
super.onCreateContextMenu(menu,v,menuInfo);
super.onCreateContextMenu(menu, v, menuInfo);
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_contact_details:
Contact contact = mSelectedUser.getContact();
if (contact != null) {
switchToContactDetails(contact);
}
return true;
case R.id.start_conversation:
startConversation(mSelectedUser);
return true;
@ -331,6 +346,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
xmppConnectionService.changeAffiliationInConference(mConversation,mSelectedUser.getJid(), MucOptions.Affiliation.OUTCAST,this);
xmppConnectionService.changeRoleInConference(mConversation,mSelectedUser.getName(), MucOptions.Role.NONE,this);
return true;
case R.id.send_private_message:
privateMsgInMuc(mConversation,mSelectedUser.getName());
return true;
default:
return super.onContextItemSelected(item);
}
@ -404,8 +422,13 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
private void updateView() {
final MucOptions mucOptions = mConversation.getMucOptions();
final User self = mucOptions.getSelf();
mAccountJid.setText(getString(R.string.using_account, mConversation
.getAccount().getJid().toBareJid()));
String account;
if (Config.DOMAIN_LOCK != null) {
account = mConversation.getAccount().getJid().getLocalpart();
} else {
account = mConversation.getAccount().getJid().toBareJid().toString();
}
mAccountJid.setText(getString(R.string.using_account, account));
mYourPhoto.setImageBitmap(avatarService().get(mConversation.getAccount(), getPixel(48)));
setTitle(mConversation.getName());
mFullJid.setText(mConversation.getJid().toBareJid().toString());

View File

@ -29,9 +29,11 @@ import android.widget.QuickContactBadge;
import android.widget.TextView;
import org.openintents.openpgp.util.OpenPgpUtils;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.entities.Account;
@ -41,12 +43,13 @@ import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.XmppConnection;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist {
public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
public static final String ACTION_VIEW_CONTACT = "view_contact";
private Contact contact;
@ -108,6 +111,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
private LinearLayout keys;
private LinearLayout tags;
private boolean showDynamicTags;
private String messageFingerprint;
private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
@ -157,6 +161,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
refreshUi();
}
@Override
public void OnUpdateBlocklist(final Status status) {
refreshUi();
}
@Override
protected void refreshUiReal() {
invalidateOptionsMenu();
@ -185,6 +194,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
} catch (final InvalidJidException ignored) {
}
}
this.messageFingerprint = getIntent().getStringExtra("fingerprint");
setContentView(R.layout.activity_contact_details);
contactJidTv = (TextView) findViewById(R.id.details_contactjid);
@ -350,7 +360,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
} else {
contactJidTv.setText(contact.getJid().toString());
}
accountJidTv.setText(getString(R.string.using_account, contact.getAccount().getJid().toBareJid()));
String account;
if (Config.DOMAIN_LOCK != null) {
account = contact.getAccount().getJid().getLocalpart();
} else {
account = contact.getAccount().getJid().toBareJid().toString();
}
accountJidTv.setText(getString(R.string.using_account, account));
badge.setImageBitmap(avatarService().get(contact, getPixel(72)));
badge.setOnClickListener(this.onBadgeClick);
@ -362,13 +378,13 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
ImageButton remove = (ImageButton) view
ImageButton removeButton = (ImageButton) view
.findViewById(R.id.button_remove);
remove.setVisibility(View.VISIBLE);
removeButton.setVisibility(View.VISIBLE);
keyType.setText("OTR Fingerprint");
key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
keys.addView(view);
remove.setOnClickListener(new OnClickListener() {
removeButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
@ -376,6 +392,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
});
}
for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
contact.getAccount(), contact.getJid().toBareJid().toString())) {
boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint);
hasKeys |= addFingerprintRow(keys, contact.getAccount(), identityKey, highlight);
}
if (contact.getPgpKeyId() != 0) {
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);
@ -460,14 +481,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd
}
@Override
public void OnUpdateBlocklist(final Status status) {
runOnUiThread(new Runnable() {
@Override
public void run() {
invalidateOptionsMenu();
populateView();
}
});
public void onKeyStatusUpdated() {
refreshUi();
}
}

View File

@ -16,6 +16,7 @@ import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.widget.SlidingPaneLayout;
import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -28,13 +29,16 @@ import android.widget.PopupMenu.OnMenuItemClickListener;
import android.widget.Toast;
import net.java.otr4j.session.SessionStatus;
import de.timroes.android.listview.EnhancedListView;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import de.timroes.android.listview.EnhancedListView;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Blockable;
import eu.siacs.conversations.entities.Contact;
@ -47,6 +51,8 @@ import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
import eu.siacs.conversations.ui.adapter.ConversationAdapter;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ConversationActivity extends XmppActivity
implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast {
@ -58,15 +64,19 @@ public class ConversationActivity extends XmppActivity
public static final String MESSAGE = "messageUuid";
public static final String TEXT = "text";
public static final String NICK = "nick";
public static final String PRIVATE_MESSAGE = "pm";
public static final int REQUEST_SEND_MESSAGE = 0x0201;
public static final int REQUEST_DECRYPT_PGP = 0x0202;
public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207;
public static final int REQUEST_TRUST_KEYS_TEXT = 0x0208;
public static final int REQUEST_TRUST_KEYS_MENU = 0x0209;
public static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301;
public static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302;
public static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303;
public static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304;
public static final int ATTACHMENT_CHOICE_LOCATION = 0x0305;
public static final int ATTACHMENT_CHOICE_INVALID = 0x0306;
private static final String STATE_OPEN_CONVERSATION = "state_open_conversation";
private static final String STATE_PANEL_OPEN = "state_panel_open";
private static final String STATE_PENDING_URI = "state_pending_uri";
@ -76,6 +86,7 @@ public class ConversationActivity extends XmppActivity
final private List<Uri> mPendingImageUris = new ArrayList<>();
final private List<Uri> mPendingFileUris = new ArrayList<>();
private Uri mPendingGeoUri = null;
private boolean forbidProcessingPendings = false;
private View mContentView;
@ -374,7 +385,7 @@ public class ConversationActivity extends XmppActivity
} else {
menuAdd.setVisible(!isConversationsOverviewHideable());
if (this.getSelectedConversation() != null) {
if (this.getSelectedConversation().getNextEncryption(forceEncryption()) != Message.ENCRYPTION_NONE) {
if (this.getSelectedConversation().getNextEncryption() != Message.ENCRYPTION_NONE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
menuSecure.setIcon(R.drawable.ic_lock_white_24dp);
} else {
@ -385,6 +396,7 @@ public class ConversationActivity extends XmppActivity
menuContactDetails.setVisible(false);
menuAttach.setVisible(getSelectedConversation().getAccount().httpUploadAvailable());
menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite());
menuSecure.setVisible(!Config.HIDE_PGP_IN_UI); //if pgp is hidden conferences have no choice of encryption
} else {
menuMucDetails.setVisible(false);
}
@ -398,7 +410,7 @@ public class ConversationActivity extends XmppActivity
return true;
}
private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
protected void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) {
final Conversation conversation = getSelectedConversation();
final Account account = conversation.getAccount();
final OnPresenceSelected callback = new OnPresenceSelected() {
@ -456,7 +468,7 @@ public class ConversationActivity extends XmppActivity
conversation.setNextCounterpart(null);
callback.onPresenceSelected();
} else {
selectPresence(conversation,callback);
selectPresence(conversation, callback);
}
}
@ -466,7 +478,7 @@ public class ConversationActivity extends XmppActivity
if (intent.resolveActivity(getPackageManager()) != null) {
return intent;
} else {
intent.setData(Uri.parse("http://play.google.com/store/apps/details?id="+packageId));
intent.setData(Uri.parse("http://play.google.com/store/apps/details?id=" + packageId));
return intent;
}
}
@ -487,7 +499,7 @@ public class ConversationActivity extends XmppActivity
break;
}
final Conversation conversation = getSelectedConversation();
final int encryption = conversation.getNextEncryption(forceEncryption());
final int encryption = conversation.getNextEncryption();
if (encryption == Message.ENCRYPTION_PGP) {
if (hasPgp()) {
if (conversation.getContact().getPgpKeyId() != 0) {
@ -534,7 +546,9 @@ public class ConversationActivity extends XmppActivity
showInstallPgpDialog();
}
} else {
selectPresenceToAttachFile(attachmentChoice,encryption);
if (encryption != Message.ENCRYPTION_AXOLOTL || !trustKeysIfNeeded(REQUEST_TRUST_KEYS_MENU, attachmentChoice)) {
selectPresenceToAttachFile(attachmentChoice, encryption);
}
}
}
@ -749,6 +763,12 @@ public class ConversationActivity extends XmppActivity
showInstallPgpDialog();
}
break;
case R.id.encryption_choice_axolotl:
Log.d(Config.LOGTAG, AxolotlService.getLogprefix(conversation.getAccount())
+ "Enabled axolotl for Contact " + conversation.getContact().getJid());
conversation.setNextEncryption(Message.ENCRYPTION_AXOLOTL);
item.setChecked(true);
break;
default:
conversation.setNextEncryption(Message.ENCRYPTION_NONE);
break;
@ -756,6 +776,7 @@ public class ConversationActivity extends XmppActivity
xmppConnectionService.databaseBackend.updateConversation(conversation);
fragment.updateChatMsgHint();
invalidateOptionsMenu();
refreshUi();
return true;
}
});
@ -763,14 +784,15 @@ public class ConversationActivity extends XmppActivity
MenuItem otr = popup.getMenu().findItem(R.id.encryption_choice_otr);
MenuItem none = popup.getMenu().findItem(R.id.encryption_choice_none);
MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp);
MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl);
pgp.setVisible(!Config.HIDE_PGP_IN_UI);
if (conversation.getMode() == Conversation.MODE_MULTI) {
otr.setEnabled(false);
} else {
if (forceEncryption()) {
none.setVisible(false);
otr.setVisible(false);
axolotl.setVisible(false);
} else if (!conversation.getAccount().getAxolotlService().isContactAxolotlCapable(conversation.getContact())) {
axolotl.setEnabled(false);
}
}
switch (conversation.getNextEncryption(forceEncryption())) {
switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_NONE:
none.setChecked(true);
break;
@ -780,6 +802,9 @@ public class ConversationActivity extends XmppActivity
case Message.ENCRYPTION_PGP:
pgp.setChecked(true);
break;
case Message.ENCRYPTION_AXOLOTL:
axolotl.setChecked(true);
break;
default:
none.setChecked(true);
break;
@ -791,8 +816,7 @@ public class ConversationActivity extends XmppActivity
protected void muteConversationDialog(final Conversation conversation) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.disable_notifications);
final int[] durations = getResources().getIntArray(
R.array.mute_options_durations);
final int[] durations = getResources().getIntArray(R.array.mute_options_durations);
builder.setItems(R.array.mute_options_descriptions,
new OnClickListener() {
@ -944,18 +968,23 @@ public class ConversationActivity extends XmppActivity
this.mConversationFragment.reInit(getSelectedConversation());
}
for(Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
attachImageToConversation(getSelectedConversation(),i.next());
if(!forbidProcessingPendings) {
for (Iterator<Uri> i = mPendingImageUris.iterator(); i.hasNext(); i.remove()) {
Uri foo = i.next();
attachImageToConversation(getSelectedConversation(), foo);
}
for(Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
attachFileToConversation(getSelectedConversation(),i.next());
for (Iterator<Uri> i = mPendingFileUris.iterator(); i.hasNext(); i.remove()) {
attachFileToConversation(getSelectedConversation(), i.next());
}
if (mPendingGeoUri != null) {
attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
mPendingGeoUri = null;
}
}
forbidProcessingPendings = false;
ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
setIntent(new Intent());
}
@ -965,10 +994,21 @@ public class ConversationActivity extends XmppActivity
final String downloadUuid = intent.getStringExtra(MESSAGE);
final String text = intent.getStringExtra(TEXT);
final String nick = intent.getStringExtra(NICK);
final boolean pm = intent.getBooleanExtra(PRIVATE_MESSAGE,false);
if (selectConversationByUuid(uuid)) {
this.mConversationFragment.reInit(getSelectedConversation());
if (nick != null) {
if (pm) {
Jid jid = getSelectedConversation().getJid();
try {
Jid next = Jid.fromParts(jid.getLocalpart(),jid.getDomainpart(),nick);
this.mConversationFragment.privateMessageWith(next);
} catch (final InvalidJidException ignored) {
//do nothing
}
} else {
this.mConversationFragment.highlightInConference(nick);
}
} else {
this.mConversationFragment.appendText(text);
}
@ -1065,6 +1105,9 @@ public class ConversationActivity extends XmppActivity
attachLocationToConversation(getSelectedConversation(), mPendingGeoUri);
this.mPendingGeoUri = null;
}
} else if (requestCode == REQUEST_TRUST_KEYS_TEXT || requestCode == REQUEST_TRUST_KEYS_MENU) {
this.forbidProcessingPendings = !xmppConnectionServiceBound;
mConversationFragment.onActivityResult(requestCode, resultCode, data);
}
} else {
mPendingImageUris.clear();
@ -1205,10 +1248,6 @@ public class ConversationActivity extends XmppActivity
});
}
public boolean forceEncryption() {
return getPreferences().getBoolean("force_encryption", false);
}
public boolean useSendButtonToIndicateStatus() {
return getPreferences().getBoolean("send_button_status", false);
}
@ -1217,6 +1256,30 @@ public class ConversationActivity extends XmppActivity
return getPreferences().getBoolean("indicate_received", false);
}
protected boolean trustKeysIfNeeded(int requestCode) {
return trustKeysIfNeeded(requestCode, ATTACHMENT_CHOICE_INVALID);
}
protected boolean trustKeysIfNeeded(int requestCode, int attachmentChoice) {
AxolotlService axolotlService = mSelectedConversation.getAccount().getAxolotlService();
boolean hasPendingKeys = !axolotlService.getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED,
mSelectedConversation.getContact()).isEmpty()
|| !axolotlService.findDevicesWithoutSession(mSelectedConversation).isEmpty();
boolean hasNoTrustedKeys = axolotlService.getNumTrustedKeys(mSelectedConversation.getContact()) == 0;
if( hasPendingKeys || hasNoTrustedKeys) {
axolotlService.createSessionsIfNeeded(mSelectedConversation);
Intent intent = new Intent(getApplicationContext(), TrustKeysActivity.class);
intent.putExtra("contact", mSelectedConversation.getContact().getJid().toBareJid().toString());
intent.putExtra("account", mSelectedConversation.getAccount().getJid().toBareJid().toString());
intent.putExtra("choice", attachmentChoice);
intent.putExtra("has_no_trusted", hasNoTrustedKeys);
startActivityForResult(intent, requestCode);
return true;
} else {
return false;
}
}
@Override
protected void refreshUiReal() {
updateConversationList();
@ -1238,6 +1301,7 @@ public class ConversationActivity extends XmppActivity
ConversationActivity.this.mConversationFragment.updateMessages();
updateActionBarTitle();
}
invalidateOptionsMenu();
}
@Override
@ -1258,12 +1322,6 @@ public class ConversationActivity extends XmppActivity
@Override
public void OnUpdateBlocklist(Status status) {
this.refreshUi();
runOnUiThread(new Runnable() {
@Override
public void run() {
invalidateOptionsMenu();
}
});
}
public void unblockConversation(final Blockable conversation) {

View File

@ -1,5 +1,6 @@
package eu.siacs.conversations.ui;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.PendingIntent;
@ -46,12 +47,12 @@ import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected;
import eu.siacs.conversations.ui.XmppActivity.OnValueEdited;
@ -292,18 +293,26 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (body.length() == 0 || this.conversation == null) {
return;
}
Message message = new Message(conversation, body, conversation.getNextEncryption(activity.forceEncryption()));
Message message = new Message(conversation, body, conversation.getNextEncryption());
if (conversation.getMode() == Conversation.MODE_MULTI) {
if (conversation.getNextCounterpart() != null) {
message.setCounterpart(conversation.getNextCounterpart());
message.setType(Message.TYPE_PRIVATE);
}
}
if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_OTR) {
switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_OTR:
sendOtrMessage(message);
} else if (conversation.getNextEncryption(activity.forceEncryption()) == Message.ENCRYPTION_PGP) {
break;
case Message.ENCRYPTION_PGP:
sendPgpMessage(message);
} else {
break;
case Message.ENCRYPTION_AXOLOTL:
if(!activity.trustKeysIfNeeded(ConversationActivity.REQUEST_TRUST_KEYS_TEXT)) {
sendAxolotlMessage(message);
}
break;
default:
sendPlainTextMessage(message);
}
}
@ -315,7 +324,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
R.string.send_private_message_to,
conversation.getNextCounterpart().getResourcepart()));
} else {
switch (conversation.getNextEncryption(activity.forceEncryption())) {
switch (conversation.getNextEncryption()) {
case Message.ENCRYPTION_NONE:
mEditMessage
.setHint(getString(R.string.send_plain_text_message));
@ -323,6 +332,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
case Message.ENCRYPTION_OTR:
mEditMessage.setHint(getString(R.string.send_otr_message));
break;
case Message.ENCRYPTION_AXOLOTL:
mEditMessage.setHint(getString(R.string.send_omemo_message));
break;
case Message.ENCRYPTION_PGP:
mEditMessage.setHint(getString(R.string.send_pgp_message));
break;
@ -377,19 +389,20 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (message.getStatus() <= Message.STATUS_RECEIVED) {
if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
if (message.getCounterpart() != null) {
if (!message.getCounterpart().isBareJid()) {
highlightInConference(message.getCounterpart().getResourcepart());
} else {
highlightInConference(message.getCounterpart().toString());
String user = message.getCounterpart().isBareJid() ? message.getCounterpart().toString() : message.getCounterpart().getResourcepart();
if (!message.getConversation().getMucOptions().isUserInRoom(user)) {
Toast.makeText(activity,activity.getString(R.string.user_has_left_conference,user),Toast.LENGTH_SHORT).show();
}
highlightInConference(user);
}
} else {
activity.switchToContactDetails(message.getContact());
activity.switchToContactDetails(message.getContact(), message.getAxolotlFingerprint());
}
} else {
Account account = message.getConversation().getAccount();
Intent intent = new Intent(activity, EditAccountActivity.class);
intent.putExtra("jid", account.getJid().toBareJid().toString());
intent.putExtra("fingerprint", message.getAxolotlFingerprint());
startActivity(intent);
}
}
@ -402,7 +415,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
if (message.getStatus() <= Message.STATUS_RECEIVED) {
if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
if (message.getCounterpart() != null) {
String user = message.getCounterpart().getResourcepart();
if (user != null) {
if (message.getConversation().getMucOptions().isUserInRoom(user)) {
privateMessageWith(message.getCounterpart());
} else {
Toast.makeText(activity, activity.getString(R.string.user_has_left_conference, user), Toast.LENGTH_SHORT).show();
}
}
}
}
} else {
@ -563,7 +583,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
private void downloadFile(Message message) {
activity.xmppConnectionService.getHttpConnectionManager()
.createNewDownloadConnection(message);
.createNewDownloadConnection(message,true);
}
private void cancelTransmission(Message message) {
@ -817,7 +837,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
} catch (final NoSuchElementException ignored) {
}
activity.xmppConnectionService.updateConversationUi();
activity.refreshUi();
}
});
}
@ -1120,6 +1140,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
builder.create().show();
}
protected void sendAxolotlMessage(final Message message) {
final ConversationActivity activity = (ConversationActivity) getActivity();
final XmppConnectionService xmppService = activity.xmppConnectionService;
xmppService.sendMessage(message);
messageSent();
}
protected void sendOtrMessage(final Message message) {
final ConversationActivity activity = (ConversationActivity) getActivity();
final XmppConnectionService xmppService = activity.xmppConnectionService;
@ -1182,4 +1209,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
updateSendButton();
}
@Override
public void onActivityResult(int requestCode, int resultCode,
final Intent data) {
if (resultCode == Activity.RESULT_OK) {
if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_TEXT) {
final String body = mEditMessage.getText().toString();
Message message = new Message(conversation, body, conversation.getNextEncryption());
sendAxolotlMessage(message);
} else if (requestCode == ConversationActivity.REQUEST_TRUST_KEYS_MENU) {
int choice = data.getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID);
activity.selectPresenceToAttachFile(choice, conversation.getNextEncryption());
}
}
}
}

View File

@ -1,6 +1,8 @@
package eu.siacs.conversations.ui;
import android.app.AlertDialog.Builder;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
@ -23,18 +25,24 @@ import android.widget.TableLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.Set;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.KnownHostsAdapter;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.XmppConnection.Features;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class EditAccountActivity extends XmppActivity implements OnAccountUpdate{
public class EditAccountActivity extends XmppActivity implements OnAccountUpdate, OnKeyStatusUpdated {
private AutoCompleteTextView mAccountJid;
private EditText mPassword;
@ -52,14 +60,23 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
private TextView mServerInfoCSI;
private TextView mServerInfoBlocking;
private TextView mServerInfoPep;
private TextView mServerInfoHttpUpload;
private TextView mSessionEst;
private TextView mOtrFingerprint;
private TextView mAxolotlFingerprint;
private TextView mAccountJidLabel;
private ImageView mAvatar;
private RelativeLayout mOtrFingerprintBox;
private RelativeLayout mAxolotlFingerprintBox;
private ImageButton mOtrFingerprintToClipboardButton;
private ImageButton mAxolotlFingerprintToClipboardButton;
private ImageButton mRegenerateAxolotlKeyButton;
private LinearLayout keys;
private LinearLayout keysCard;
private Jid jidToEdit;
private Account mAccount;
private String messageFingerprint;
private boolean mFetchingAvatar = false;
@ -72,17 +89,34 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
xmppConnectionService.updateAccount(mAccount);
return;
}
final boolean registerNewAccount = mRegisterNew.isChecked();
final boolean registerNewAccount = mRegisterNew.isChecked() && !Config.DISALLOW_REGISTRATION_IN_UI;
if (Config.DOMAIN_LOCK != null && mAccountJid.getText().toString().contains("@")) {
mAccountJid.setError(getString(R.string.invalid_username));
mAccountJid.requestFocus();
return;
}
final Jid jid;
try {
if (Config.DOMAIN_LOCK != null) {
jid = Jid.fromParts(mAccountJid.getText().toString(),Config.DOMAIN_LOCK,null);
} else {
jid = Jid.fromString(mAccountJid.getText().toString());
}
} catch (final InvalidJidException e) {
if (Config.DOMAIN_LOCK != null) {
mAccountJid.setError(getString(R.string.invalid_username));
} else {
mAccountJid.setError(getString(R.string.invalid_jid));
}
mAccountJid.requestFocus();
return;
}
if (jid.isDomainJid()) {
if (Config.DOMAIN_LOCK != null) {
mAccountJid.setError(getString(R.string.invalid_username));
} else {
mAccountJid.setError(getString(R.string.invalid_jid));
}
mAccountJid.requestFocus();
return;
}
@ -108,15 +142,11 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount);
xmppConnectionService.updateAccount(mAccount);
} else {
try {
if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) {
if (xmppConnectionService.findAccountByJid(jid) != null) {
mAccountJid.setError(getString(R.string.account_already_exists));
mAccountJid.requestFocus();
return;
}
} catch (final InvalidJidException e) {
return;
}
mAccount = new Account(jid.toBareJid(), password);
mAccount.setOption(Account.OPTION_USETLS, true);
mAccount.setOption(Account.OPTION_USECOMPRESSION, true);
@ -139,12 +169,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
finish();
}
};
@Override
public void onAccountUpdate() {
runOnUiThread(new Runnable() {
@Override
public void run() {
public void refreshUiReal() {
invalidateOptionsMenu();
if (mAccount != null
&& mAccount.getStatus() != Account.State.ONLINE
@ -166,7 +192,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
updateAccountInformation(false);
}
}
});
@Override
public void onAccountUpdate() {
refreshUi();
}
private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() {
@ -271,10 +300,17 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
protected boolean accountInfoEdited() {
return this.mAccount != null && (!this.mAccount.getJid().toBareJid().toString().equals(
this.mAccountJid.getText().toString())
|| !this.mAccount.getPassword().equals(
this.mPassword.getText().toString()));
if (this.mAccount == null) {
return false;
}
final String unmodified;
if (Config.DOMAIN_LOCK != null) {
unmodified = this.mAccount.getJid().getLocalpart();
} else {
unmodified = this.mAccount.getJid().toBareJid().toString();
}
return !unmodified.equals(this.mAccountJid.getText().toString()) ||
!this.mAccount.getPassword().equals(this.mPassword.getText().toString());
}
@Override
@ -292,6 +328,11 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
setContentView(R.layout.activity_edit_account);
this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid);
this.mAccountJid.addTextChangedListener(this.mTextWatcher);
this.mAccountJidLabel = (TextView) findViewById(R.id.account_jid_label);
if (Config.DOMAIN_LOCK != null) {
this.mAccountJidLabel.setText(R.string.username);
this.mAccountJid.setHint(R.string.username_hint);
}
this.mPassword = (EditText) findViewById(R.id.account_password);
this.mPassword.addTextChangedListener(this.mTextWatcher);
this.mPasswordConfirm = (EditText) findViewById(R.id.account_password_confirm);
@ -307,9 +348,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mServerInfoBlocking = (TextView) findViewById(R.id.server_info_blocking);
this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm);
this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
this.mServerInfoHttpUpload = (TextView) findViewById(R.id.server_info_http_upload);
this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box);
this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
this.mAxolotlFingerprint = (TextView) findViewById(R.id.axolotl_fingerprint);
this.mAxolotlFingerprintBox = (RelativeLayout) findViewById(R.id.axolotl_fingerprint_box);
this.mAxolotlFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_axolotl_to_clipboard);
this.mRegenerateAxolotlKeyButton = (ImageButton) findViewById(R.id.action_regenerate_axolotl_key);
this.keysCard = (LinearLayout) findViewById(R.id.other_device_keys_card);
this.keys = (LinearLayout) findViewById(R.id.other_device_keys);
this.mSaveButton = (Button) findViewById(R.id.save_button);
this.mCancelButton = (Button) findViewById(R.id.cancel_button);
this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener);
@ -328,6 +376,9 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
}
};
this.mRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword);
if (Config.DISALLOW_REGISTRATION_IN_UI) {
this.mRegisterNew.setVisibility(View.GONE);
}
}
@Override
@ -338,6 +389,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list);
final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more);
final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server);
final MenuItem clearDevices = menu.findItem(R.id.action_clear_devices);
if (mAccount != null && mAccount.isOnlineAndConnected()) {
if (!mAccount.getXmppConnection().getFeatures().blocking()) {
showBlocklist.setVisible(false);
@ -345,11 +397,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
if (!mAccount.getXmppConnection().getFeatures().register()) {
changePassword.setVisible(false);
}
Set<Integer> otherDevices = mAccount.getAxolotlService().getOwnDeviceIds();
if (otherDevices == null || otherDevices.isEmpty()) {
clearDevices.setVisible(false);
}
} else {
showQrCode.setVisible(false);
showBlocklist.setVisible(false);
showMoreInfo.setVisible(false);
changePassword.setVisible(false);
clearDevices.setVisible(false);
}
return true;
}
@ -363,6 +420,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} catch (final InvalidJidException | NullPointerException ignored) {
this.jidToEdit = null;
}
this.messageFingerprint = getIntent().getStringExtra("fingerprint");
if (this.jidToEdit != null) {
this.mRegisterNew.setVisibility(View.GONE);
if (getActionBar() != null) {
@ -379,9 +437,6 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
protected void onBackendConnected() {
final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
android.R.layout.simple_list_item_1,
xmppConnectionService.getKnownHosts());
if (this.jidToEdit != null) {
this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit);
updateAccountInformation(true);
@ -394,7 +449,12 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mCancelButton.setEnabled(false);
this.mCancelButton.setTextColor(getSecondaryTextColor());
}
if (Config.DOMAIN_LOCK == null) {
final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this,
android.R.layout.simple_list_item_1,
xmppConnectionService.getKnownHosts());
this.mAccountJid.setAdapter(mKnownHostsAdapter);
}
updateSaveButton();
}
@ -415,13 +475,20 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
changePasswordIntent.putExtra("account", mAccount.getJid().toString());
startActivity(changePasswordIntent);
break;
case R.id.action_clear_devices:
showWipePepDialog();
break;
}
return super.onOptionsItemSelected(item);
}
private void updateAccountInformation(boolean init) {
if (init) {
if (Config.DOMAIN_LOCK != null) {
this.mAccountJid.setText(this.mAccount.getJid().getLocalpart());
} else {
this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString());
}
this.mPassword.setText(this.mAccount.getPassword());
}
if (this.jidToEdit != null) {
@ -477,10 +544,15 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} else {
this.mServerInfoPep.setText(R.string.server_info_unavailable);
}
final String fingerprint = this.mAccount.getOtrFingerprint();
if (fingerprint != null) {
if (features.httpUpload()) {
this.mServerInfoHttpUpload.setText(R.string.server_info_available);
} else {
this.mServerInfoHttpUpload.setText(R.string.server_info_unavailable);
}
final String otrFingerprint = this.mAccount.getOtrFingerprint();
if (otrFingerprint != null) {
this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint));
this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
this.mOtrFingerprintToClipboardButton
.setVisibility(View.VISIBLE);
this.mOtrFingerprintToClipboardButton
@ -489,7 +561,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
@Override
public void onClick(final View v) {
if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) {
if (copyTextToClipboard(otrFingerprint, R.string.otr_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_otr_fingerprint,
@ -500,6 +572,57 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
} else {
this.mOtrFingerprintBox.setVisibility(View.GONE);
}
final String axolotlFingerprint = this.mAccount.getAxolotlService().getOwnPublicKey().getFingerprint();
if (axolotlFingerprint != null) {
this.mAxolotlFingerprintBox.setVisibility(View.VISIBLE);
this.mAxolotlFingerprint.setText(CryptoHelper.prettifyFingerprint(axolotlFingerprint));
this.mAxolotlFingerprintToClipboardButton
.setVisibility(View.VISIBLE);
this.mAxolotlFingerprintToClipboardButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
if (copyTextToClipboard(axolotlFingerprint, R.string.omemo_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_omemo_fingerprint,
Toast.LENGTH_SHORT).show();
}
}
});
if (Config.SHOW_REGENERATE_AXOLOTL_KEYS_BUTTON) {
this.mRegenerateAxolotlKeyButton
.setVisibility(View.VISIBLE);
this.mRegenerateAxolotlKeyButton
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(final View v) {
showRegenerateAxolotlKeyDialog();
}
});
}
} else {
this.mAxolotlFingerprintBox.setVisibility(View.GONE);
}
final IdentityKey ownKey = mAccount.getAxolotlService().getOwnPublicKey();
boolean hasKeys = false;
keys.removeAllViews();
for(final IdentityKey identityKey : xmppConnectionService.databaseBackend.loadIdentityKeys(
mAccount, mAccount.getJid().toBareJid().toString())) {
if(ownKey.equals(identityKey)) {
continue;
}
boolean highlight = identityKey.getFingerprint().replaceAll("\\s", "").equals(messageFingerprint);
hasKeys |= addFingerprintRow(keys, mAccount, identityKey, highlight);
}
if (hasKeys) {
keysCard.setVisibility(View.VISIBLE);
} else {
keysCard.setVisibility(View.GONE);
}
} else {
if (this.mAccount.errorStatus()) {
this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId()));
@ -512,4 +635,41 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate
this.mStats.setVisibility(View.GONE);
}
}
public void showRegenerateAxolotlKeyDialog() {
Builder builder = new Builder(this);
builder.setTitle("Regenerate Key");
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage("Are you sure you want to regenerate your Identity Key? (This will also wipe all established sessions and contact Identity Keys)");
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton("Yes",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAccount.getAxolotlService().regenerateKeys();
}
});
builder.create().show();
}
public void showWipePepDialog() {
Builder builder = new Builder(this);
builder.setTitle(getString(R.string.clear_other_devices));
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage(getString(R.string.clear_other_devices_desc));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.accept),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
mAccount.getAxolotlService().wipeOtherPepDevices();
}
});
builder.create().show();
}
@Override
public void onKeyStatusUpdated() {
refreshUi();
}
}

View File

@ -1,27 +1,29 @@
package eu.siacs.conversations.ui;
import java.util.ArrayList;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.AccountAdapter;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.os.Bundle;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ListView;
import java.util.ArrayList;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
import eu.siacs.conversations.ui.adapter.AccountAdapter;
public class ManageAccountActivity extends XmppActivity implements OnAccountUpdate {
protected Account selectedAccount = null;
@ -80,6 +82,7 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda
menu.findItem(R.id.mgmt_account_publish_avatar).setVisible(false);
} else {
menu.findItem(R.id.mgmt_account_enable).setVisible(false);
menu.findItem(R.id.mgmt_account_announce_pgp).setVisible(!Config.HIDE_PGP_IN_UI);
}
menu.setHeaderTitle(this.selectedAccount.getJid().toBareJid().toString());
}

View File

@ -13,6 +13,7 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.utils.PhoneHelper;
@ -192,7 +193,13 @@ public class PublishProfilePictureActivity extends XmppActivity {
} else {
loadImageIntoPreview(avatarUri);
}
this.accountTextView.setText(this.account.getJid().toBareJid().toString());
String account;
if (Config.DOMAIN_LOCK != null) {
account = this.account.getJid().getLocalpart();
} else {
account = this.account.getJid().toBareJid().toString();
}
this.accountTextView.setText(account);
}
}
@ -251,4 +258,8 @@ public class PublishProfilePictureActivity extends XmppActivity {
this.publishButton.setTextColor(getSecondaryTextColor());
}
public void refreshUiReal() {
//nothing to do. This Activity doesn't implement any listeners
}
}

View File

@ -1,19 +1,6 @@
package eu.siacs.conversations.ui;
import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xmpp.XmppConnection;
import android.app.AlertDialog;
import android.app.Fragment;
import android.app.FragmentManager;
import android.content.DialogInterface;
import android.content.SharedPreferences;
@ -25,6 +12,17 @@ import android.preference.Preference;
import android.preference.PreferenceManager;
import android.widget.Toast;
import java.security.KeyStoreException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import de.duenndns.ssl.MemorizingTrustManager;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.xmpp.XmppConnection;
public class SettingsActivity extends XmppActivity implements
OnSharedPreferenceChangeListener {
private SettingsFragment mSettingsFragment;
@ -182,4 +180,8 @@ public class SettingsActivity extends XmppActivity implements
}
}
public void refreshUiReal() {
//nothing to do. This Activity doesn't implement any listeners
}
}

View File

@ -17,7 +17,6 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
@ -229,4 +228,8 @@ public class ShareWithActivity extends XmppActivity {
}
public void refreshUiReal() {
//nothing to do. This Activity doesn't implement any listeners
}
}

View File

@ -289,7 +289,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
protected void toggleContactBlock() {
final int position = contact_context_id;
BlockContactDialog.show(this, xmppConnectionService, (Contact)contacts.get(position));
BlockContactDialog.show(this, xmppConnectionService, (Contact) contacts.get(position));
}
protected void deleteContact() {
@ -368,7 +368,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
final Jid accountJid;
try {
if (Config.DOMAIN_LOCK != null) {
accountJid = Jid.fromParts((String) spinner.getSelectedItem(),Config.DOMAIN_LOCK,null);
} else {
accountJid = Jid.fromString((String) spinner.getSelectedItem());
}
} catch (final InvalidJidException e) {
return;
}
@ -379,8 +383,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
jid.setError(getString(R.string.invalid_jid));
return;
}
final Account account = xmppConnectionService
.findAccountByJid(accountJid);
final Account account = xmppConnectionService.findAccountByJid(accountJid);
if (account == null) {
dialog.dismiss();
return;
@ -428,7 +431,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
}
final Jid accountJid;
try {
if (Config.DOMAIN_LOCK != null) {
accountJid = Jid.fromParts((String) spinner.getSelectedItem(),Config.DOMAIN_LOCK,null);
} else {
accountJid = Jid.fromString((String) spinner.getSelectedItem());
}
} catch (final InvalidJidException e) {
return;
}
@ -576,9 +583,13 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU
this.mActivatedAccounts.clear();
for (Account account : xmppConnectionService.getAccounts()) {
if (account.getStatus() != Account.State.DISABLED) {
if (Config.DOMAIN_LOCK != null) {
this.mActivatedAccounts.add(account.getJid().getLocalpart());
} else {
this.mActivatedAccounts.add(account.getJid().toBareJid().toString());
}
}
}
final Intent intent = getIntent();
final ActionBar ab = getActionBar();
if (intent != null && intent.getBooleanExtra("init",false) && ab != null) {

View File

@ -0,0 +1,255 @@
package eu.siacs.conversations.ui;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.CompoundButton;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.whispersystems.libaxolotl.IdentityKey;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
public class TrustKeysActivity extends XmppActivity implements OnKeyStatusUpdated {
private Jid accountJid;
private Jid contactJid;
private boolean hasOtherTrustedKeys = false;
private boolean hasPendingFetches = false;
private boolean hasNoTrustedKeys = true;
private Contact contact;
private TextView ownKeysTitle;
private LinearLayout ownKeys;
private LinearLayout ownKeysCard;
private TextView foreignKeysTitle;
private LinearLayout foreignKeys;
private LinearLayout foreignKeysCard;
private Button mSaveButton;
private Button mCancelButton;
private final Map<IdentityKey, Boolean> ownKeysToTrust = new HashMap<>();
private final Map<IdentityKey, Boolean> foreignKeysToTrust = new HashMap<>();
private final OnClickListener mSaveButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
commitTrusts();
Intent data = new Intent();
data.putExtra("choice", getIntent().getIntExtra("choice", ConversationActivity.ATTACHMENT_CHOICE_INVALID));
setResult(RESULT_OK, data);
finish();
}
};
private final OnClickListener mCancelButtonListener = new OnClickListener() {
@Override
public void onClick(View v) {
setResult(RESULT_CANCELED);
finish();
}
};
@Override
protected void refreshUiReal() {
invalidateOptionsMenu();
populateView();
}
@Override
protected String getShareableUri() {
if (contact != null) {
return contact.getShareableUri();
} else {
return "";
}
}
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_trust_keys);
try {
this.accountJid = Jid.fromString(getIntent().getExtras().getString("account"));
} catch (final InvalidJidException ignored) {
}
try {
this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact"));
} catch (final InvalidJidException ignored) {
}
hasNoTrustedKeys = getIntent().getBooleanExtra("has_no_trusted", false);
ownKeysTitle = (TextView) findViewById(R.id.own_keys_title);
ownKeys = (LinearLayout) findViewById(R.id.own_keys_details);
ownKeysCard = (LinearLayout) findViewById(R.id.own_keys_card);
foreignKeysTitle = (TextView) findViewById(R.id.foreign_keys_title);
foreignKeys = (LinearLayout) findViewById(R.id.foreign_keys_details);
foreignKeysCard = (LinearLayout) findViewById(R.id.foreign_keys_card);
mCancelButton = (Button) findViewById(R.id.cancel_button);
mCancelButton.setOnClickListener(mCancelButtonListener);
mSaveButton = (Button) findViewById(R.id.save_button);
mSaveButton.setOnClickListener(mSaveButtonListener);
if (getActionBar() != null) {
getActionBar().setHomeButtonEnabled(true);
getActionBar().setDisplayHomeAsUpEnabled(true);
}
}
private void populateView() {
setTitle(getString(R.string.trust_omemo_fingerprints));
ownKeys.removeAllViews();
foreignKeys.removeAllViews();
boolean hasOwnKeys = false;
boolean hasForeignKeys = false;
for(final IdentityKey identityKey : ownKeysToTrust.keySet()) {
hasOwnKeys = true;
addFingerprintRowWithListeners(ownKeys, contact.getAccount(), identityKey, false,
XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey)), false,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
ownKeysToTrust.put(identityKey, isChecked);
// own fingerprints have no impact on locked status.
}
},
null
);
}
for(final IdentityKey identityKey : foreignKeysToTrust.keySet()) {
hasForeignKeys = true;
addFingerprintRowWithListeners(foreignKeys, contact.getAccount(), identityKey, false,
XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey)), false,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
foreignKeysToTrust.put(identityKey, isChecked);
lockOrUnlockAsNeeded();
}
},
null
);
}
if(hasOwnKeys) {
ownKeysTitle.setText(accountJid.toString());
ownKeysCard.setVisibility(View.VISIBLE);
}
if(hasForeignKeys) {
foreignKeysTitle.setText(contactJid.toString());
foreignKeysCard.setVisibility(View.VISIBLE);
}
if(hasPendingFetches) {
setFetching();
lock();
} else {
lockOrUnlockAsNeeded();
setDone();
}
}
private void getFingerprints(final Account account) {
Set<IdentityKey> ownKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED);
Set<IdentityKey> foreignKeysSet = account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNDECIDED, contact);
if (hasNoTrustedKeys) {
ownKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED));
foreignKeysSet.addAll(account.getAxolotlService().getKeysWithTrust(XmppAxolotlSession.Trust.UNTRUSTED, contact));
}
for(final IdentityKey identityKey : ownKeysSet) {
if(!ownKeysToTrust.containsKey(identityKey)) {
ownKeysToTrust.put(identityKey, false);
}
}
for(final IdentityKey identityKey : foreignKeysSet) {
if(!foreignKeysToTrust.containsKey(identityKey)) {
foreignKeysToTrust.put(identityKey, false);
}
}
}
@Override
public void onBackendConnected() {
if ((accountJid != null) && (contactJid != null)) {
final Account account = xmppConnectionService
.findAccountByJid(accountJid);
if (account == null) {
return;
}
this.contact = account.getRoster().getContact(contactJid);
ownKeysToTrust.clear();
foreignKeysToTrust.clear();
getFingerprints(account);
if(account.getAxolotlService().getNumTrustedKeys(contact) > 0) {
hasOtherTrustedKeys = true;
}
Conversation conversation = xmppConnectionService.findOrCreateConversation(account, contactJid, false);
if(account.getAxolotlService().hasPendingKeyFetches(conversation)) {
hasPendingFetches = true;
}
populateView();
}
}
@Override
public void onKeyStatusUpdated() {
final Account account = xmppConnectionService.findAccountByJid(accountJid);
hasPendingFetches = false;
getFingerprints(account);
refreshUi();
}
private void commitTrusts() {
for(IdentityKey identityKey:ownKeysToTrust.keySet()) {
contact.getAccount().getAxolotlService().setFingerprintTrust(
identityKey.getFingerprint().replaceAll("\\s", ""),
XmppAxolotlSession.Trust.fromBoolean(ownKeysToTrust.get(identityKey)));
}
for(IdentityKey identityKey:foreignKeysToTrust.keySet()) {
contact.getAccount().getAxolotlService().setFingerprintTrust(
identityKey.getFingerprint().replaceAll("\\s", ""),
XmppAxolotlSession.Trust.fromBoolean(foreignKeysToTrust.get(identityKey)));
}
}
private void unlock() {
mSaveButton.setEnabled(true);
mSaveButton.setTextColor(getPrimaryTextColor());
}
private void lock() {
mSaveButton.setEnabled(false);
mSaveButton.setTextColor(getSecondaryTextColor());
}
private void lockOrUnlockAsNeeded() {
if (!hasOtherTrustedKeys && !foreignKeysToTrust.values().contains(true)){
lock();
} else {
unlock();
}
}
private void setDone() {
mSaveButton.setText(getString(R.string.done));
}
private void setFetching() {
mSaveButton.setText(getString(R.string.fetching_keys));
}
}

View File

@ -43,8 +43,11 @@ import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import com.google.zxing.BarcodeFormat;
@ -56,6 +59,8 @@ import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import net.java.otr4j.session.SessionID;
import org.whispersystems.libaxolotl.IdentityKey;
import java.io.FileNotFoundException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
@ -65,6 +70,7 @@ import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
@ -74,7 +80,10 @@ import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.ui.widget.Switch;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.ExceptionHelper;
import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
@ -90,6 +99,7 @@ public abstract class XmppActivity extends Activity {
protected int mPrimaryTextColor;
protected int mSecondaryTextColor;
protected int mTertiaryTextColor;
protected int mPrimaryBackgroundColor;
protected int mSecondaryBackgroundColor;
protected int mColorRed;
@ -116,7 +126,7 @@ public abstract class XmppActivity extends Activity {
protected ConferenceInvite mPendingConferenceInvite = null;
protected void refreshUi() {
protected final void refreshUi() {
final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
if (diff > Config.REFRESH_UI_INTERVAL) {
mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
@ -128,9 +138,7 @@ public abstract class XmppActivity extends Activity {
}
}
protected void refreshUiReal() {
};
abstract protected void refreshUiReal();
protected interface OnValueEdited {
public void onValueEdited(String value);
@ -287,6 +295,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
}
if (this instanceof OnKeyStatusUpdated) {
this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
}
}
protected void unregisterListeners() {
@ -308,6 +319,9 @@ public abstract class XmppActivity extends Activity {
if (this instanceof XmppConnectionService.OnShowErrorToast) {
this.xmppConnectionService.removeOnShowErrorToastListener();
}
if (this instanceof OnKeyStatusUpdated) {
this.xmppConnectionService.removeOnNewKeysAvailableListener();
}
}
@Override
@ -336,7 +350,8 @@ public abstract class XmppActivity extends Activity {
ExceptionHelper.init(getApplicationContext());
mPrimaryTextColor = getResources().getColor(R.color.black87);
mSecondaryTextColor = getResources().getColor(R.color.black54);
mColorRed = getResources().getColor(R.color.red500);
mTertiaryTextColor = getResources().getColor(R.color.black12);
mColorRed = getResources().getColor(R.color.red800);
mColorOrange = getResources().getColor(R.color.orange500);
mColorGreen = getResources().getColor(R.color.green500);
mPrimaryColor = getResources().getColor(R.color.green500);
@ -371,14 +386,18 @@ public abstract class XmppActivity extends Activity {
public void switchToConversation(Conversation conversation, String text,
boolean newTask) {
switchToConversation(conversation,text,null,newTask);
switchToConversation(conversation,text,null,false,newTask);
}
public void highlightInMuc(Conversation conversation, String nick) {
switchToConversation(conversation, null, nick, false);
switchToConversation(conversation, null, nick, false, false);
}
private void switchToConversation(Conversation conversation, String text, String nick, boolean newTask) {
public void privateMsgInMuc(Conversation conversation, String nick) {
switchToConversation(conversation, null, nick, true, false);
}
private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
Intent viewConversationIntent = new Intent(this,
ConversationActivity.class);
viewConversationIntent.setAction(Intent.ACTION_VIEW);
@ -389,6 +408,7 @@ public abstract class XmppActivity extends Activity {
}
if (nick != null) {
viewConversationIntent.putExtra(ConversationActivity.NICK, nick);
viewConversationIntent.putExtra(ConversationActivity.PRIVATE_MESSAGE,pm);
}
viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
if (newTask) {
@ -404,10 +424,15 @@ public abstract class XmppActivity extends Activity {
}
public void switchToContactDetails(Contact contact) {
switchToContactDetails(contact, null);
}
public void switchToContactDetails(Contact contact, String messageFingerprint) {
Intent intent = new Intent(this, ContactDetailsActivity.class);
intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString());
intent.putExtra("contact", contact.getJid().toString());
intent.putExtra("fingerprint", messageFingerprint);
startActivity(intent);
}
@ -588,6 +613,124 @@ public abstract class XmppActivity extends Activity {
builder.create().show();
}
protected boolean addFingerprintRow(LinearLayout keys, final Account account, IdentityKey identityKey, boolean highlight) {
final String fingerprint = identityKey.getFingerprint().replaceAll("\\s", "");
final XmppAxolotlSession.Trust trust = account.getAxolotlService()
.getFingerprintTrust(fingerprint);
return addFingerprintRowWithListeners(keys, account, identityKey, highlight, trust, true,
new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
account.getAxolotlService().setFingerprintTrust(fingerprint,
(isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
XmppAxolotlSession.Trust.UNTRUSTED);
}
},
new View.OnClickListener() {
@Override
public void onClick(View v) {
account.getAxolotlService().setFingerprintTrust(fingerprint,
XmppAxolotlSession.Trust.UNTRUSTED);
v.setEnabled(true);
}
}
);
}
protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
final IdentityKey identityKey,
boolean highlight,
XmppAxolotlSession.Trust trust,
boolean showTag,
CompoundButton.OnCheckedChangeListener
onCheckedChangeListener,
View.OnClickListener onClickListener) {
if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
return false;
}
View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
trustToggle.setVisibility(View.VISIBLE);
trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
trustToggle.setOnClickListener(onClickListener);
view.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
showPurgeKeyDialog(account, identityKey);
return true;
}
});
switch (trust) {
case UNTRUSTED:
case TRUSTED:
trustToggle.setChecked(trust == XmppAxolotlSession.Trust.TRUSTED, false);
trustToggle.setEnabled(true);
key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor());
break;
case UNDECIDED:
trustToggle.setChecked(false, false);
trustToggle.setEnabled(false);
key.setTextColor(getPrimaryTextColor());
keyType.setTextColor(getSecondaryTextColor());
break;
case INACTIVE_UNTRUSTED:
case INACTIVE_UNDECIDED:
trustToggle.setOnClickListener(null);
trustToggle.setChecked(false, false);
trustToggle.setEnabled(false);
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
break;
case INACTIVE_TRUSTED:
trustToggle.setOnClickListener(null);
trustToggle.setChecked(true, false);
trustToggle.setEnabled(false);
key.setTextColor(getTertiaryTextColor());
keyType.setTextColor(getTertiaryTextColor());
break;
}
if (showTag) {
keyType.setText(getString(R.string.omemo_fingerprint));
} else {
keyType.setVisibility(View.GONE);
}
if (highlight) {
keyType.setTextColor(getResources().getColor(R.color.accent));
keyType.setText(getString(R.string.omemo_fingerprint_selected_message));
} else {
keyType.setText(getString(R.string.omemo_fingerprint));
}
key.setText(CryptoHelper.prettifyFingerprint(identityKey.getFingerprint()));
keys.addView(view);
return true;
}
public void showPurgeKeyDialog(final Account account, final IdentityKey identityKey) {
Builder builder = new Builder(this);
builder.setTitle(getString(R.string.purge_key));
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage(getString(R.string.purge_key_desc_part1)
+ "\n\n" + CryptoHelper.prettifyFingerprint(identityKey.getFingerprint())
+ "\n\n" + getString(R.string.purge_key_desc_part2));
builder.setNegativeButton(getString(R.string.cancel), null);
builder.setPositiveButton(getString(R.string.accept),
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
account.getAxolotlService().purgeKey(identityKey);
refreshUi();
}
});
builder.create().show();
}
public void selectPresence(final Conversation conversation,
final OnPresenceSelected listener) {
final Contact contact = conversation.getContact();
@ -707,6 +850,10 @@ public abstract class XmppActivity extends Activity {
}
};
public int getTertiaryTextColor() {
return this.mTertiaryTextColor;
}
public int getSecondaryTextColor() {
return this.mSecondaryTextColor;
}

View File

@ -1,11 +1,5 @@
package eu.siacs.conversations.ui.adapter;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.ManageAccountActivity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
@ -14,7 +8,15 @@ import android.widget.ArrayAdapter;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Switch;
import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.ui.ManageAccountActivity;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.ui.widget.Switch;
public class AccountAdapter extends ArrayAdapter<Account> {
@ -34,7 +36,11 @@ public class AccountAdapter extends ArrayAdapter<Account> {
view = inflater.inflate(R.layout.account_row, parent, false);
}
TextView jid = (TextView) view.findViewById(R.id.account_jid);
if (Config.DOMAIN_LOCK != null) {
jid.setText(account.getJid().getLocalpart());
} else {
jid.setText(account.getJid().toBareJid().toString());
}
TextView statusView = (TextView) view.findViewById(R.id.account_status);
ImageView imageView = (ImageView) view.findViewById(R.id.account_image);
imageView.setImageBitmap(activity.avatarService().get(account, activity.getPixel(48)));
@ -53,8 +59,7 @@ public class AccountAdapter extends ArrayAdapter<Account> {
}
final Switch tglAccountState = (Switch) view.findViewById(R.id.tgl_account_status);
final boolean isDisabled = (account.getStatus() == Account.State.DISABLED);
tglAccountState.setOnCheckedChangeListener(null);
tglAccountState.setChecked(!isDisabled);
tglAccountState.setChecked(!isDisabled,false);
tglAccountState.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean b) {

View File

@ -21,8 +21,8 @@ import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;

View File

@ -1,13 +1,13 @@
package eu.siacs.conversations.ui.adapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.Filter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class KnownHostsAdapter extends ArrayAdapter<String> {
private ArrayList<String> domains;
private Filter domainFilter = new Filter() {

View File

@ -1,15 +1,5 @@
package eu.siacs.conversations.ui.adapter;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
@ -26,6 +16,16 @@ import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.ui.XmppActivity;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.jid.Jid;
public class ListItemAdapter extends ArrayAdapter<ListItem> {
protected XmppActivity activity;

View File

@ -3,6 +3,7 @@ package eu.siacs.conversations.ui.adapter;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Color;
import android.graphics.Typeface;
import android.net.Uri;
import android.text.Spannable;
@ -26,13 +27,14 @@ import android.widget.Toast;
import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.FileParams;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.UIHelper;
@ -79,18 +81,30 @@ public class MessageAdapter extends ArrayAdapter<Message> {
return 3;
}
public int getItemViewType(Message message) {
if (message.getType() == Message.TYPE_STATUS) {
return STATUS;
} else if (message.getStatus() <= Message.STATUS_RECEIVED) {
return RECEIVED;
}
return SENT;
}
@Override
public int getItemViewType(int position) {
if (getItem(position).getType() == Message.TYPE_STATUS) {
return STATUS;
} else if (getItem(position).getStatus() <= Message.STATUS_RECEIVED) {
return RECEIVED;
return this.getItemViewType(getItem(position));
}
private int getMessageTextColor(int type, boolean primary) {
if (type == SENT) {
return activity.getResources().getColor(primary ? R.color.black87 : R.color.black54);
} else {
return SENT;
return activity.getResources().getColor(primary ? R.color.white : R.color.white70);
}
}
private void displayStatus(ViewHolder viewHolder, Message message) {
private void displayStatus(ViewHolder viewHolder, Message message, int type) {
String filesize = null;
String info = null;
boolean error = false;
@ -145,15 +159,39 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
break;
}
if (error) {
if (error && type == SENT) {
viewHolder.time.setTextColor(activity.getWarningTextColor());
} else {
viewHolder.time.setTextColor(activity.getSecondaryTextColor());
viewHolder.time.setTextColor(this.getMessageTextColor(type,false));
}
if (message.getEncryption() == Message.ENCRYPTION_NONE) {
viewHolder.indicator.setVisibility(View.GONE);
} else {
viewHolder.indicator.setVisibility(View.VISIBLE);
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
XmppAxolotlSession.Trust trust = message.getConversation()
.getAccount().getAxolotlService().getFingerprintTrust(
message.getAxolotlFingerprint());
if(trust == null || trust != XmppAxolotlSession.Trust.TRUSTED) {
viewHolder.indicator.setColorFilter(activity.getWarningTextColor());
viewHolder.indicator.setAlpha(1.0f);
} else {
viewHolder.indicator.clearColorFilter();
if (type == SENT) {
viewHolder.indicator.setAlpha(0.57f);
} else {
viewHolder.indicator.setAlpha(0.7f);
}
}
} else {
viewHolder.indicator.clearColorFilter();
if (type == SENT) {
viewHolder.indicator.setAlpha(0.57f);
} else {
viewHolder.indicator.setAlpha(0.7f);
}
}
}
String formatedTime = UIHelper.readableTimeDifferenceFull(getContext(),
@ -185,19 +223,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
}
private void displayInfoMessage(ViewHolder viewHolder, String text) {
private void displayInfoMessage(ViewHolder viewHolder, String text, int type) {
if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE);
}
viewHolder.image.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.VISIBLE);
viewHolder.messageBody.setText(text);
viewHolder.messageBody.setTextColor(activity.getSecondaryTextColor());
viewHolder.messageBody.setTextColor(getMessageTextColor(type,false));
viewHolder.messageBody.setTypeface(null, Typeface.ITALIC);
viewHolder.messageBody.setTextIsSelectable(false);
}
private void displayDecryptionFailed(ViewHolder viewHolder) {
private void displayDecryptionFailed(ViewHolder viewHolder, int type) {
if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE);
}
@ -205,7 +243,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setVisibility(View.VISIBLE);
viewHolder.messageBody.setText(getContext().getString(
R.string.decryption_failed));
viewHolder.messageBody.setTextColor(activity.getWarningTextColor());
viewHolder.messageBody.setTextColor(getMessageTextColor(type,false));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
viewHolder.messageBody.setTextIsSelectable(false);
}
@ -223,7 +261,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setText(span);
}
private void displayTextMessage(final ViewHolder viewHolder, final Message message) {
private void displayTextMessage(final ViewHolder viewHolder, final Message message, int type) {
if (viewHolder.download_button != null) {
viewHolder.download_button.setVisibility(View.GONE);
}
@ -265,8 +303,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
final Spannable span = new SpannableString(privateMarker + " "
+ formattedBody);
span.setSpan(new ForegroundColorSpan(activity
.getSecondaryTextColor()), 0, privateMarker
span.setSpan(new ForegroundColorSpan(getMessageTextColor(type,false)), 0, privateMarker
.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
span.setSpan(new StyleSpan(Typeface.BOLD), 0,
privateMarker.length(),
@ -281,7 +318,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else {
viewHolder.messageBody.setText("");
}
viewHolder.messageBody.setTextColor(activity.getPrimaryTextColor());
viewHolder.messageBody.setTextColor(this.getMessageTextColor(type,true));
viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
viewHolder.messageBody.setTextIsSelectable(true);
}
@ -350,17 +387,15 @@ public class MessageAdapter extends ArrayAdapter<Message> {
scalledW = (int) target;
scalledH = (int) (params.height / ((double) params.width / target));
}
viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
scalledW, scalledH));
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(scalledW, scalledH);
layoutParams.setMargins(0, (int)(metrics.density * 4), 0, (int)(metrics.density * 4));
viewHolder.image.setLayoutParams(layoutParams);
activity.loadBitmap(message, viewHolder.image);
viewHolder.image.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(activity.xmppConnectionService
.getFileBackend().getJingleFileUri(message), "image/*");
getContext().startActivity(intent);
openDownloadable(message);
}
});
viewHolder.image.setOnLongClickListener(openContextMenu);
@ -489,7 +524,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (transferable.getStatus() == Transferable.STATUS_OFFER_CHECK_FILESIZE) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)));
} else {
displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first);
displayInfoMessage(viewHolder, UIHelper.getMessagePreview(activity, message).first,type);
}
} else if (message.getType() == Message.TYPE_IMAGE && message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED) {
displayImageMessage(viewHolder, message);
@ -501,10 +536,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
if (activity.hasPgp()) {
displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message));
displayInfoMessage(viewHolder,activity.getString(R.string.encrypted_message),type);
} else {
displayInfoMessage(viewHolder,
activity.getString(R.string.install_openkeychain));
displayInfoMessage(viewHolder,activity.getString(R.string.install_openkeychain),type);
if (viewHolder != null) {
viewHolder.message_box
.setOnClickListener(new OnClickListener() {
@ -517,7 +551,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
}
} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
displayDecryptionFailed(viewHolder);
displayDecryptionFailed(viewHolder,type);
} else {
if (GeoHelper.isGeoUri(message.getBody())) {
displayLocationMessage(viewHolder,message);
@ -526,11 +560,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
} else if (message.treatAsDownloadable() == Message.Decision.MUST) {
displayDownloadableMessage(viewHolder, message, activity.getString(R.string.check_x_filesize, UIHelper.getFileDescriptionString(activity, message)));
} else {
displayTextMessage(viewHolder, message);
displayTextMessage(viewHolder, message, type);
}
}
displayStatus(viewHolder, message);
if (type == RECEIVED) {
if(message.isValidInSession()) {
viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received);
} else {
viewHolder.message_box.setBackgroundResource(R.drawable.message_bubble_received_warning);
}
}
displayStatus(viewHolder, message, type);
return view;
}
@ -543,7 +585,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
Toast.LENGTH_SHORT).show();
}
} else if (message.treatAsDownloadable() != Message.Decision.NEVER) {
activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message);
activity.xmppConnectionService.getHttpConnectionManager().createNewDownloadConnection(message,true);
}
}

View File

@ -0,0 +1,68 @@
package eu.siacs.conversations.ui.widget;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import com.kyleduo.switchbutton.SwitchButton;
public class Switch extends SwitchButton {
private int mTouchSlop;
private int mClickTimeout;
private float mStartX;
private float mStartY;
private OnClickListener mOnClickListener;
public Switch(Context context) {
super(context);
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
}
public Switch(Context context, AttributeSet attrs) {
super(context, attrs);
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
}
public Switch(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
mClickTimeout = ViewConfiguration.getPressedStateDuration() + ViewConfiguration.getTapTimeout();
}
@Override
public void setOnClickListener(OnClickListener onClickListener) {
this.mOnClickListener = onClickListener;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
float deltaX = event.getX() - mStartX;
float deltaY = event.getY() - mStartY;
int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
mStartX = event.getX();
mStartY = event.getY();
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
float time = event.getEventTime() - event.getDownTime();
if (deltaX < mTouchSlop && deltaY < mTouchSlop && time < mClickTimeout) {
if (mOnClickListener != null) {
this.mOnClickListener.onClick(this);
}
}
break;
default:
break;
}
return true;
}
return super.onTouchEvent(event);
}
}

View File

@ -96,11 +96,10 @@ public final class CryptoHelper {
} else if (fingerprint.length() < 40) {
return fingerprint;
}
StringBuilder builder = new StringBuilder(fingerprint);
builder.insert(8, " ");
builder.insert(17, " ");
builder.insert(26, " ");
builder.insert(35, " ");
StringBuilder builder = new StringBuilder(fingerprint.replaceAll("\\s",""));
for(int i=8;i<builder.length();i+=9) {
builder.insert(i, ' ');
}
return builder.toString();
}

View File

@ -1,17 +1,7 @@
package eu.siacs.conversations.utils;
import de.measite.minidns.Client;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.Record;
import de.measite.minidns.Record.TYPE;
import de.measite.minidns.Record.CLASS;
import de.measite.minidns.record.SRV;
import de.measite.minidns.record.A;
import de.measite.minidns.record.AAAA;
import de.measite.minidns.record.Data;
import de.measite.minidns.util.NameUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.os.Bundle;
import android.util.Log;
import java.io.IOException;
import java.net.InetAddress;
@ -22,8 +12,18 @@ import java.util.Random;
import java.util.TreeMap;
import java.util.regex.Pattern;
import android.os.Bundle;
import android.util.Log;
import de.measite.minidns.Client;
import de.measite.minidns.DNSMessage;
import de.measite.minidns.Record;
import de.measite.minidns.Record.CLASS;
import de.measite.minidns.Record.TYPE;
import de.measite.minidns.record.A;
import de.measite.minidns.record.AAAA;
import de.measite.minidns.record.Data;
import de.measite.minidns.record.SRV;
import de.measite.minidns.util.NameUtil;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.xmpp.jid.Jid;
public class DNSHelper {
@ -38,17 +38,14 @@ public class DNSHelper {
public static Bundle getSRVRecord(final Jid jid) throws IOException {
final String host = jid.getDomainpart();
String dns[] = client.findDNS();
if (dns != null) {
for (String dnsserver : dns) {
InetAddress ip = InetAddress.getByName(dnsserver);
for (int i = 0; i < dns.length; ++i) {
InetAddress ip = InetAddress.getByName(dns[i]);
Bundle b = queryDNS(host, ip);
if (b.containsKey("values")) {
if (b.containsKey("values") || i == dns.length - 1) {
return b;
}
}
}
return queryDNS(host, InetAddress.getByName("8.8.8.8"));
return null;
}
public static Bundle queryDNS(String host, InetAddress dnsServer) {
@ -132,7 +129,6 @@ public class DNSHelper {
} catch (SocketTimeoutException e) {
bundle.putString("error", "timeout");
} catch (Exception e) {
e.printStackTrace();
bundle.putString("error", "unhandled");
}
return bundle;

View File

@ -1,5 +1,7 @@
package eu.siacs.conversations.utils;
import android.content.Context;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
@ -8,8 +10,6 @@ import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import android.content.Context;
public class ExceptionHandler implements UncaughtExceptionHandler {
private UncaughtExceptionHandler defaultHandler;

View File

@ -1,5 +1,17 @@
package eu.siacs.conversations.utils;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
import android.text.format.DateUtils;
import android.util.Log;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
@ -15,18 +27,6 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xmpp.jid.InvalidJidException;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.DialogInterface.OnClickListener;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.preference.PreferenceManager;
import android.text.format.DateUtils;
import android.util.Log;
public class ExceptionHelper {
public static void init(Context context) {
if (!(Thread.getDefaultUncaughtExceptionHandler() instanceof ExceptionHandler)) {

View File

@ -0,0 +1,144 @@
package eu.siacs.conversations.utils;
import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
public class FileUtils {
/**
* Get a file path from a Uri. This will get the the path for Storage Access
* Framework Documents, as well as the _data field for the MediaStore and
* other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @author paulburke
*/
@SuppressLint("NewApi")
public static String getPath(final Context context, final Uri uri) {
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
// DocumentProvider
if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
if ("primary".equalsIgnoreCase(type)) {
return Environment.getExternalStorageDirectory() + "/" + split[1];
}
// TODO handle non-primary volumes
}
// DownloadsProvider
else if (isDownloadsDocument(uri)) {
final String id = DocumentsContract.getDocumentId(uri);
final Uri contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
}
// MediaProvider
else if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
final String selection = "_id=?";
final String[] selectionArgs = new String[]{
split[1]
};
return getDataColumn(context, contentUri, selection, selectionArgs);
}
}
// MediaStore (and general)
else if ("content".equalsIgnoreCase(uri.getScheme())) {
return getDataColumn(context, uri, null, null);
}
// File
else if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
return null;
}
/**
* Get the value of the data column for this Uri. This is useful for
* MediaStore Uris, and other file-based ContentProviders.
*
* @param context The context.
* @param uri The Uri to query.
* @param selection (Optional) Filter used in the query.
* @param selectionArgs (Optional) Selection arguments used in the query.
* @return The value of the _data column, which is typically a file path.
*/
public static String getDataColumn(Context context, Uri uri, String selection,
String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {
column
};
try {
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
null);
if (cursor != null && cursor.moveToFirst()) {
final int column_index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(column_index);
}
} finally {
if (cursor != null)
cursor.close();
}
return null;
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is ExternalStorageProvider.
*/
public static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is DownloadsProvider.
*/
public static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
/**
* @param uri The Uri to check.
* @return Whether the Uri authority is MediaProvider.
*/
public static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
}

View File

@ -1,9 +1,9 @@
package eu.siacs.conversations.utils;
import java.util.List;
import android.os.Bundle;
import java.util.List;
public interface OnPhoneContactsLoadedListener {
public void onPhoneContactsLoaded(List<Bundle> phoneContacts);
}

View File

@ -1,9 +1,5 @@
package eu.siacs.conversations.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
import android.content.Context;
import android.content.CursorLoader;
import android.content.Loader;
@ -15,6 +11,9 @@ import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.Profile;
import java.util.List;
import java.util.concurrent.RejectedExecutionException;
public class PhoneHelper {
public static void loadPhoneContacts(Context context,final List<Bundle> phoneContacts, final OnPhoneContactsLoadedListener listener) {

View File

@ -1,5 +1,10 @@
package eu.siacs.conversations.utils;
import android.content.Context;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Pair;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
@ -9,15 +14,10 @@ import java.util.Locale;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.content.Context;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.Pair;
public class UIHelper {
private static String BLACK_HEART_SUIT = "\u2665";

View File

@ -21,6 +21,11 @@ public class Element {
this.name = name;
}
public Element(String name, String xmlns) {
this.name = name;
this.setAttribute("xmlns", xmlns);
}
public Element addChild(Element child) {
this.content = null;
children.add(child);

View File

@ -1,19 +1,19 @@
package eu.siacs.conversations.xml;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import eu.siacs.conversations.Config;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import android.util.Xml;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import eu.siacs.conversations.Config;
public class XmlReader {
private XmlPullParser parser;
private PowerManager.WakeLock wakeLock;

View File

@ -0,0 +1,5 @@
package eu.siacs.conversations.xmpp;
public interface OnKeyStatusUpdated {
public void onKeyStatusUpdated();
}

View File

@ -26,7 +26,6 @@ import java.net.IDN;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
@ -35,6 +34,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
@ -81,7 +81,6 @@ public class XmppConnection implements Runnable {
private static final int PACKET_IQ = 0;
private static final int PACKET_MESSAGE = 1;
private static final int PACKET_PRESENCE = 2;
private final Context applicationContext;
protected Account account;
private final WakeLock wakeLock;
private Socket socket;
@ -95,7 +94,7 @@ public class XmppConnection implements Runnable {
private String streamId = null;
private int smVersion = 3;
private final SparseArray<String> messageReceipts = new SparseArray<>();
private final SparseArray<String> mStanzaReceipts = new SparseArray<>();
private int stanzasReceived = 0;
private int stanzasSent = 0;
@ -123,7 +122,6 @@ public class XmppConnection implements Runnable {
PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString());
tagWriter = new TagWriter();
mXmppConnectionService = service;
applicationContext = service.getApplicationContext();
}
protected void changeStatus(final Account.State nextStatus) {
@ -165,6 +163,9 @@ public class XmppConnection implements Runnable {
}
} else {
final Bundle result = DNSHelper.getSRVRecord(account.getServer());
if (result == null) {
throw new IOException("unhandled exception in DNS resolver");
}
final ArrayList<Parcelable> values = result.getParcelableArrayList("values");
if ("timeout".equals(result.getString("error"))) {
throw new IOException("timeout in dns");
@ -338,23 +339,24 @@ public class XmppConnection implements Runnable {
+ ": session resumed with lost packages");
stanzasSent = serverCount;
} else {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString()
+ ": session resumed");
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": session resumed");
}
if (acknowledgedListener != null) {
for (int i = 0; i < messageReceipts.size(); ++i) {
if (serverCount >= messageReceipts.keyAt(i)) {
acknowledgedListener.onMessageAcknowledged(
account, messageReceipts.valueAt(i));
acknowledgeStanzaUpTo(serverCount);
ArrayList<IqPacket> failedIqPackets = new ArrayList<>();
for(int i = 0; i < this.mStanzaReceipts.size(); ++i) {
String id = mStanzaReceipts.valueAt(i);
Pair<IqPacket,OnIqPacketReceived> pair = id == null ? null : this.packetCallbacks.get(id);
if (pair != null) {
failedIqPackets.add(pair.first);
}
}
mStanzaReceipts.clear();
Log.d(Config.LOGTAG,"resending "+failedIqPackets.size()+" iq stanza");
for(IqPacket packet : failedIqPackets) {
sendUnmodifiedIqPacket(packet,null);
}
messageReceipts.clear();
} catch (final NumberFormatException ignored) {
}
sendServiceDiscoveryInfo(account.getServer());
sendServiceDiscoveryInfo(account.getJid().toBareJid());
sendServiceDiscoveryItems(account.getServer());
sendInitialPing();
} else if (nextTag.isStart("r")) {
tagReader.readElement(nextTag);
@ -368,17 +370,7 @@ public class XmppConnection implements Runnable {
lastPacketReceived = SystemClock.elapsedRealtime();
try {
final int serverSequence = Integer.parseInt(ack.getAttribute("h"));
if (Config.EXTENDED_SM_LOGGING) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + serverSequence);
}
final String msgId = this.messageReceipts.get(serverSequence);
if (msgId != null) {
if (this.acknowledgedListener != null) {
this.acknowledgedListener.onMessageAcknowledged(
account, msgId);
}
this.messageReceipts.remove(serverSequence);
}
acknowledgeStanzaUpTo(serverSequence);
} catch (NumberFormatException e) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server send ack without sequence number");
}
@ -406,6 +398,22 @@ public class XmppConnection implements Runnable {
}
}
private void acknowledgeStanzaUpTo(int serverCount) {
for (int i = 0; i < mStanzaReceipts.size(); ++i) {
if (serverCount >= mStanzaReceipts.keyAt(i)) {
if (Config.EXTENDED_SM_LOGGING) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server acknowledged stanza #" + mStanzaReceipts.keyAt(i));
}
String id = mStanzaReceipts.valueAt(i);
if (acknowledgedListener != null) {
acknowledgedListener.onMessageAcknowledged(account, id);
}
mStanzaReceipts.removeAt(i);
i--;
}
}
}
private void sendInitialPing() {
Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping");
final IqPacket iq = new IqPacket(IqPacket.TYPE.GET);
@ -521,14 +529,6 @@ public class XmppConnection implements Runnable {
tagWriter.writeTag(startTLS);
}
private SharedPreferences getPreferences() {
return PreferenceManager.getDefaultSharedPreferences(applicationContext);
}
private boolean enableLegacySSL() {
return getPreferences().getBoolean("enable_legacy_ssl", false);
}
private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException {
tagReader.readTag();
try {
@ -707,6 +707,7 @@ public class XmppConnection implements Runnable {
} catch (final InterruptedException ignored) {
}
}
clearIqCallbacks();
final IqPacket iq = new IqPacket(IqPacket.TYPE.SET);
iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind")
.addChild("resource").setContent(account.getResource());
@ -737,9 +738,20 @@ public class XmppConnection implements Runnable {
});
}
private void clearIqCallbacks() {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": clearing iq iq callbacks");
final IqPacket failurePacket = new IqPacket(IqPacket.TYPE.ERROR);
Iterator<Entry<String, Pair<IqPacket, OnIqPacketReceived>>> iterator = this.packetCallbacks.entrySet().iterator();
while(iterator.hasNext()) {
Entry<String, Pair<IqPacket, OnIqPacketReceived>> entry = iterator.next();
entry.getValue().second.onIqPacketReceived(account,failurePacket);
iterator.remove();
}
}
private void sendStartSession() {
final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET);
startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session");
startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session");
this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
@ -763,7 +775,7 @@ public class XmppConnection implements Runnable {
final EnablePacket enable = new EnablePacket(smVersion);
tagWriter.writeStanzaAsync(enable);
stanzasSent = 0;
messageReceipts.clear();
mStanzaReceipts.clear();
}
features.carbonsEnabled = false;
features.blockListRequested = false;
@ -895,7 +907,7 @@ public class XmppConnection implements Runnable {
public void sendIqPacket(final IqPacket packet, final OnIqPacketReceived callback) {
packet.setFrom(account.getJid());
this.sendUnmodifiedIqPacket(packet,callback);
this.sendUnmodifiedIqPacket(packet, callback);
}
@ -905,9 +917,6 @@ public class XmppConnection implements Runnable {
packet.setAttribute("id", id);
}
if (callback != null) {
if (packet.getId() == null) {
packet.setId(nextRandomId());
}
packetCallbacks.put(packet.getId(), new Pair<>(packet, callback));
}
this.sendPacket(packet);
@ -932,11 +941,11 @@ public class XmppConnection implements Runnable {
++stanzasSent;
}
tagWriter.writeStanzaAsync(packet);
if (packet instanceof MessagePacket && packet.getId() != null && getFeatures().sm()) {
if ((packet instanceof MessagePacket || packet instanceof IqPacket) && packet.getId() != null && this.streamId != null) {
if (Config.EXTENDED_SM_LOGGING) {
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for message stanza #" + stanzasSent);
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": requesting ack for stanza #" + stanzasSent);
}
this.messageReceipts.put(stanzasSent, packet.getId());
this.mStanzaReceipts.put(stanzasSent, packet.getId());
tagWriter.writeStanzaAsync(new RequestPacket(this.smVersion));
}
}
@ -1005,7 +1014,7 @@ public class XmppConnection implements Runnable {
if (tagWriter.isActive()) {
tagWriter.finish();
try {
while (!tagWriter.finished()) {
while (!tagWriter.finished() && socket.isConnected()) {
Log.d(Config.LOGTAG, "not yet finished");
Thread.sleep(100);
}

View File

@ -1,5 +1,13 @@
package eu.siacs.conversations.xmpp.jingle;
import android.content.Intent;
import android.net.Uri;
import android.util.Log;
import android.util.Pair;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
@ -8,17 +16,18 @@ import java.util.Locale;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import android.content.Intent;
import android.net.Uri;
import android.os.SystemClock;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.OnMessageCreatedCallback;
import eu.siacs.conversations.crypto.axolotl.XmppAxolotlMessage;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
@ -59,15 +68,19 @@ public class JingleConnection implements Transferable {
private String contentCreator;
private int mProgress = 0;
private long mLastGuiRefresh = 0;
private boolean receivedCandidate = false;
private boolean sentCandidate = false;
private boolean acceptedAutomatically = false;
private XmppAxolotlMessage mXmppAxolotlMessage;
private JingleTransport transport = null;
private OutputStream mFileOutputStream;
private InputStream mFileInputStream;
private OnIqPacketReceived responseListener = new OnIqPacketReceived() {
@Override
@ -84,15 +97,13 @@ public class JingleConnection implements Transferable {
public void onFileTransmitted(DownloadableFile file) {
if (responder.equals(account.getJid())) {
sendSuccess();
if (acceptedAutomatically) {
message.markUnread();
JingleConnection.this.mXmppConnectionService
.getNotificationService().push(message);
}
mXmppConnectionService.getFileBackend().updateFileParams(message);
mXmppConnectionService.databaseBackend.createMessage(message);
mXmppConnectionService.markMessage(message,
Message.STATUS_RECEIVED);
mXmppConnectionService.markMessage(message,Message.STATUS_RECEIVED);
if (acceptedAutomatically) {
message.markUnread();
JingleConnection.this.mXmppConnectionService.getNotificationService().push(message);
}
} else {
if (message.getEncryption() == Message.ENCRYPTION_PGP || message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
file.delete();
@ -113,6 +124,14 @@ public class JingleConnection implements Transferable {
}
};
public InputStream getFileInputStream() {
return this.mFileInputStream;
}
public OutputStream getFileOutputStream() {
return this.mFileOutputStream;
}
private OnProxyActivated onProxyActivated = new OnProxyActivated() {
@Override
@ -194,7 +213,22 @@ public class JingleConnection implements Transferable {
mXmppConnectionService.sendIqPacket(account,response,null);
}
public void init(Message message) {
public void init(final Message message) {
if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
Conversation conversation = message.getConversation();
conversation.getAccount().getAxolotlService().prepareKeyTransportMessage(conversation.getContact(), new OnMessageCreatedCallback() {
@Override
public void run(XmppAxolotlMessage xmppAxolotlMessage) {
init(message, xmppAxolotlMessage);
}
});
} else {
init(message, null);
}
}
private void init(Message message, XmppAxolotlMessage xmppAxolotlMessage) {
this.mXmppAxolotlMessage = xmppAxolotlMessage;
this.contentCreator = "initiator";
this.contentName = this.mJingleConnectionManager.nextRandomId();
this.message = message;
@ -238,8 +272,7 @@ public class JingleConnection implements Transferable {
});
mergeCandidate(candidate);
} else {
Log.d(Config.LOGTAG,
"no primary candidate of our own was found");
Log.d(Config.LOGTAG, "no primary candidate of our own was found");
sendInitRequest();
}
}
@ -267,13 +300,16 @@ public class JingleConnection implements Transferable {
this.contentCreator = content.getAttribute("creator");
this.contentName = content.getAttribute("name");
this.transportId = content.getTransportId();
this.mergeCandidates(JingleCandidate.parse(content.socks5transport()
.getChildren()));
this.mergeCandidates(JingleCandidate.parse(content.socks5transport().getChildren()));
this.fileOffer = packet.getJingleContent().getFileOffer();
mXmppConnectionService.sendIqPacket(account,packet.generateResponse(IqPacket.TYPE.RESULT),null);
if (fileOffer != null) {
Element encrypted = fileOffer.findChild("encrypted", AxolotlService.PEP_PREFIX);
if (encrypted != null) {
this.mXmppAxolotlMessage = XmppAxolotlMessage.fromElement(encrypted, packet.getFrom().toBareJid());
}
Element fileSize = fileOffer.findChild("size");
Element fileNameElement = fileOffer.findChild("name");
if (fileNameElement != null) {
@ -319,10 +355,8 @@ public class JingleConnection implements Transferable {
message.setBody(Long.toString(size));
conversation.add(message);
mXmppConnectionService.updateConversationUi();
if (size < this.mJingleConnectionManager
.getAutoAcceptFileSize()) {
Log.d(Config.LOGTAG, "auto accepting file from "
+ packet.getFrom());
if (size < this.mJingleConnectionManager.getAutoAcceptFileSize()) {
Log.d(Config.LOGTAG, "auto accepting file from "+ packet.getFrom());
this.acceptedAutomatically = true;
this.sendAccept();
} else {
@ -333,22 +367,36 @@ public class JingleConnection implements Transferable {
+ " allowed size:"
+ this.mJingleConnectionManager
.getAutoAcceptFileSize());
this.mXmppConnectionService.getNotificationService()
.push(message);
this.mXmppConnectionService.getNotificationService().push(message);
}
this.file = this.mXmppConnectionService.getFileBackend()
.getFile(message, false);
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
if (mXmppAxolotlMessage != null) {
XmppAxolotlMessage.XmppAxolotlKeyTransportMessage transportMessage = account.getAxolotlService().processReceivingKeyTransportMessage(mXmppAxolotlMessage);
if (transportMessage != null) {
message.setEncryption(Message.ENCRYPTION_AXOLOTL);
this.file.setKey(transportMessage.getKey());
this.file.setIv(transportMessage.getIv());
message.setAxolotlFingerprint(transportMessage.getFingerprint());
} else {
Log.d(Config.LOGTAG,"could not process KeyTransportMessage");
}
} else if (message.getEncryption() == Message.ENCRYPTION_OTR) {
byte[] key = conversation.getSymmetricKey();
if (key == null) {
this.sendCancel();
this.fail();
return;
} else {
this.file.setKey(key);
this.file.setKeyAndIv(key);
}
}
this.mFileOutputStream = AbstractConnectionManager.createOutputStream(this.file,message.getEncryption() == Message.ENCRYPTION_AXOLOTL);
if (message.getEncryption() == Message.ENCRYPTION_OTR && Config.REPORT_WRONG_FILESIZE_IN_OTR_JINGLE) {
this.file.setExpectedSize((size / 16 + 1) * 16);
} else {
this.file.setExpectedSize(size);
}
Log.d(Config.LOGTAG, "receiving file: expecting size of " + this.file.getExpectedSize());
} else {
this.sendCancel();
this.fail();
@ -364,19 +412,35 @@ public class JingleConnection implements Transferable {
Content content = new Content(this.contentCreator, this.contentName);
if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) {
content.setTransportId(this.transportId);
this.file = this.mXmppConnectionService.getFileBackend().getFile(
message, false);
this.file = this.mXmppConnectionService.getFileBackend().getFile(message, false);
Pair<InputStream,Integer> pair;
try {
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
Conversation conversation = this.message.getConversation();
if (!this.mXmppConnectionService.renewSymmetricKey(conversation)) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not set symmetric key");
Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not set symmetric key");
cancel();
}
this.file.setKeyAndIv(conversation.getSymmetricKey());
pair = AbstractConnectionManager.createInputStream(this.file, false);
this.file.setExpectedSize(pair.second);
content.setFileOffer(this.file, true);
this.file.setKey(conversation.getSymmetricKey());
} else if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) {
this.file.setKey(mXmppAxolotlMessage.getInnerKey());
this.file.setIv(mXmppAxolotlMessage.getIV());
pair = AbstractConnectionManager.createInputStream(this.file, true);
this.file.setExpectedSize(pair.second);
content.setFileOffer(this.file, false).addChild(mXmppAxolotlMessage.toElement());
} else {
pair = AbstractConnectionManager.createInputStream(this.file, false);
this.file.setExpectedSize(pair.second);
content.setFileOffer(this.file, false);
}
} catch (FileNotFoundException e) {
cancel();
return;
}
this.mFileInputStream = pair.first;
this.transportId = this.mJingleConnectionManager.nextRandomId();
content.setTransportId(this.transportId);
content.socks5transport().setChildren(getCandidatesAsElements());
@ -748,6 +812,8 @@ public class JingleConnection implements Transferable {
if (this.transport != null && this.transport instanceof JingleInbandTransport) {
this.transport.disconnect();
}
FileBackend.close(mFileInputStream);
FileBackend.close(mFileOutputStream);
if (this.message != null) {
if (this.responder.equals(account.getJid())) {
this.message.setTransferable(new TransferablePlaceholder(Transferable.STATUS_FAILED));
@ -901,11 +967,8 @@ public class JingleConnection implements Transferable {
public void updateProgress(int i) {
this.mProgress = i;
if (SystemClock.elapsedRealtime() - this.mLastGuiRefresh > Config.PROGRESS_UI_UPDATE_INTERVAL) {
this.mLastGuiRefresh = SystemClock.elapsedRealtime();
mXmppConnectionService.updateConversationUi();
}
}
interface OnProxyActivated {
public void success();
@ -956,4 +1019,8 @@ public class JingleConnection implements Transferable {
public int getProgress() {
return this.mProgress;
}
public AbstractConnectionManager getConnectionManager() {
return this.mJingleConnectionManager;
}
}

View File

@ -1,16 +1,18 @@
package eu.siacs.conversations.xmpp.jingle;
import android.annotation.SuppressLint;
import android.util.Log;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import android.annotation.SuppressLint;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Transferable;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.Xmlns;

View File

@ -1,5 +1,8 @@
package eu.siacs.conversations.xmpp.jingle;
import android.util.Base64;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -7,9 +10,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import android.util.Base64;
import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
@ -93,7 +93,7 @@ public class JingleInbandTransport extends JingleTransport {
digest.reset();
file.getParentFile().mkdirs();
file.createNewFile();
this.fileOutputStream = file.createOutputStream();
this.fileOutputStream = connection.getFileOutputStream();
if (this.fileOutputStream == null) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not create output stream");
callback.onFileTransferAborted();
@ -112,15 +112,11 @@ public class JingleInbandTransport extends JingleTransport {
this.onFileTransmissionStatusChanged = callback;
this.file = file;
try {
if (this.file.getKey() != null) {
this.remainingSize = (this.file.getSize() / 16 + 1) * 16;
} else {
this.remainingSize = this.file.getSize();
}
this.remainingSize = this.file.getExpectedSize();
this.fileSize = this.remainingSize;
this.digest = MessageDigest.getInstance("SHA-1");
this.digest.reset();
fileInputStream = this.file.createInputStream();
fileInputStream = connection.getFileInputStream();
if (fileInputStream == null) {
Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could no create input stream");
callback.onFileTransferAborted();

View File

@ -1,5 +1,6 @@
package eu.siacs.conversations.xmpp.jingle;
import android.os.PowerManager;
import android.util.Log;
import java.io.FileNotFoundException;
@ -96,23 +97,24 @@ public class JingleSocks5Transport extends JingleTransport {
}
public void send(final DownloadableFile file,
final OnFileTransmissionStatusChanged callback) {
public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) {
new Thread(new Runnable() {
@Override
public void run() {
InputStream fileInputStream = null;
final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_send_"+connection.getSessionId());
try {
wakeLock.acquire();
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
fileInputStream = file.createInputStream();
fileInputStream = connection.getFileInputStream();
if (fileInputStream == null) {
Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create input stream");
callback.onFileTransferAborted();
return;
}
long size = file.getSize();
long size = file.getExpectedSize();
long transmitted = 0;
int count;
byte[] buffer = new byte[8192];
@ -138,6 +140,7 @@ public class JingleSocks5Transport extends JingleTransport {
callback.onFileTransferAborted();
} finally {
FileBackend.close(fileInputStream);
wakeLock.release();
}
}
}).start();
@ -150,14 +153,16 @@ public class JingleSocks5Transport extends JingleTransport {
@Override
public void run() {
OutputStream fileOutputStream = null;
final PowerManager.WakeLock wakeLock = connection.getConnectionManager().createWakeLock("jingle_receive_"+connection.getSessionId());
try {
wakeLock.acquire();
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
inputStream.skip(45);
socket.setSoTimeout(30000);
file.getParentFile().mkdirs();
file.createNewFile();
fileOutputStream = file.createOutputStream();
fileOutputStream = connection.getFileOutputStream();
if (fileOutputStream == null) {
callback.onFileTransferAborted();
Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": could not create output stream");
@ -166,7 +171,7 @@ public class JingleSocks5Transport extends JingleTransport {
double size = file.getExpectedSize();
long remainingSize = file.getExpectedSize();
byte[] buffer = new byte[8192];
int count = buffer.length;
int count;
while (remainingSize > 0) {
count = inputStream.read(buffer);
if (count == -1) {
@ -194,7 +199,9 @@ public class JingleSocks5Transport extends JingleTransport {
Log.d(Config.LOGTAG, connection.getAccount().getJid().toBareJid() + ": "+e.getMessage());
callback.onFileTransferAborted();
} finally {
wakeLock.release();
FileBackend.close(fileOutputStream);
FileBackend.close(inputStream);
}
}
}).start();
@ -209,27 +216,9 @@ public class JingleSocks5Transport extends JingleTransport {
}
public void disconnect() {
if (this.outputStream != null) {
try {
this.outputStream.close();
} catch (IOException e) {
}
}
if (this.inputStream != null) {
try {
this.inputStream.close();
} catch (IOException e) {
}
}
if (this.socket != null) {
try {
this.socket.close();
} catch (IOException e) {
}
}
FileBackend.close(inputStream);
FileBackend.close(outputStream);
FileBackend.close(socket);
}
public boolean isEstablished() {

View File

@ -1,5 +1,31 @@
package eu.siacs.conversations.xmpp.jingle;
import android.util.Log;
import android.util.Pair;
import org.bouncycastle.crypto.engines.AESEngine;
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.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
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 eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.DownloadableFile;
public abstract class JingleTransport {

View File

@ -25,17 +25,18 @@ public class Content extends Element {
this.transportId = sid;
}
public void setFileOffer(DownloadableFile actualFile, boolean otr) {
public Element setFileOffer(DownloadableFile actualFile, boolean otr) {
Element description = this.addChild("description",
"urn:xmpp:jingle:apps:file-transfer:3");
Element offer = description.addChild("offer");
Element file = offer.addChild("file");
file.addChild("size").setContent(Long.toString(actualFile.getSize()));
file.addChild("size").setContent(Long.toString(actualFile.getExpectedSize()));
if (otr) {
file.addChild("name").setContent(actualFile.getName() + ".otr");
} else {
file.addChild("name").setContent(actualFile.getName());
}
return file;
}
public Element getFileOffer() {

View File

@ -1,10 +1,10 @@
package eu.siacs.conversations.xmpp.pep;
import android.util.Base64;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jid.Jid;
import android.util.Base64;
public class Avatar {
public enum Origin { PEP, VCARD };

View File

@ -39,6 +39,9 @@ public class IqPacket extends AbstractStanza {
public TYPE getType() {
final String type = getAttribute("type");
if (type == null) {
return TYPE.INVALID;
}
switch (type) {
case "error":
return TYPE.ERROR;

View File

@ -2,8 +2,6 @@ package eu.siacs.conversations.xmpp.stanzas;
import android.util.Pair;
import java.text.ParseException;
import eu.siacs.conversations.parser.AbstractParser;
import eu.siacs.conversations.xml.Element;
@ -29,6 +27,11 @@ public class MessagePacket extends AbstractStanza {
this.children.add(0, body);
}
public void setAxolotlMessage(Element axolotlMessage) {
this.children.remove(findChild("body"));
this.children.add(0, axolotlMessage);
}
public void setType(int type) {
switch (type) {
case TYPE_CHAT:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 177 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 B

After

Width:  |  Height:  |  Size: 294 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 757 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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