Merge branch 'development'
|
@ -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
|
||||
|
|
19
README.md
|
@ -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/)
|
||||
|
|
156
art/md_switch_thumb_disable.svg
Normal 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 |
153
art/md_switch_thumb_off_normal.svg
Normal 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 |
159
art/md_switch_thumb_off_pressed.svg
Normal 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 KiB |
146
art/md_switch_thumb_on_normal.svg
Normal 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 |
162
art/md_switch_thumb_on_pressed.svg
Normal 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 KiB |
165
art/message_bubble_received.svg
Normal file
|
@ -0,0 +1,165 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="36"
|
||||
height="26"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
sodipodi:docname="message_bubble_received.svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<filter
|
||||
x="-0.25"
|
||||
y="-0.25"
|
||||
width="1.5"
|
||||
height="1.5"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter3811"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood
|
||||
flood-opacity="0.25"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood3813" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite3815" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
result="blur"
|
||||
id="feGaussianBlur3817" />
|
||||
<feOffset
|
||||
dx="0"
|
||||
dy="1"
|
||||
result="offset"
|
||||
id="feOffset3819" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite3821" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="25.745257"
|
||||
inkscape:cy="9.618802"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:window-width="989"
|
||||
inkscape:window-height="755"
|
||||
inkscape:window-x="22"
|
||||
inkscape:window-y="16"
|
||||
inkscape:window-maximized="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
guidecolor="#000000"
|
||||
guideopacity="0.49803922">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid2985"
|
||||
empspacing="4"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true"
|
||||
spacingx="1px"
|
||||
spacingy="1px"
|
||||
originx="0px"
|
||||
originy="0px"
|
||||
color="#0000ff"
|
||||
opacity="0.03137255" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="20,26"
|
||||
id="guide3060" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="24,26"
|
||||
id="guide3062" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,22"
|
||||
id="guide3064" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,6"
|
||||
id="guide3066" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="26,0"
|
||||
id="guide3068" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="18,0"
|
||||
id="guide3070" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,10"
|
||||
id="guide3074" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,8"
|
||||
id="guide3076" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer"
|
||||
transform="translate(0,-2)">
|
||||
<g
|
||||
id="g3759"
|
||||
style="fill:#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 |
165
art/message_bubble_received_warning.svg
Normal file
|
@ -0,0 +1,165 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="36"
|
||||
height="26"
|
||||
id="svg2"
|
||||
version="1.1"
|
||||
inkscape:version="0.48.5 r10040"
|
||||
sodipodi:docname="message_bubble_received.svg">
|
||||
<defs
|
||||
id="defs4">
|
||||
<filter
|
||||
x="-0.25"
|
||||
y="-0.25"
|
||||
width="1.5"
|
||||
height="1.5"
|
||||
inkscape:label="Drop Shadow"
|
||||
id="filter3811"
|
||||
color-interpolation-filters="sRGB">
|
||||
<feFlood
|
||||
flood-opacity="0.25"
|
||||
flood-color="rgb(0,0,0)"
|
||||
result="flood"
|
||||
id="feFlood3813" />
|
||||
<feComposite
|
||||
in="flood"
|
||||
in2="SourceGraphic"
|
||||
operator="in"
|
||||
result="composite1"
|
||||
id="feComposite3815" />
|
||||
<feGaussianBlur
|
||||
stdDeviation="0.5"
|
||||
result="blur"
|
||||
id="feGaussianBlur3817" />
|
||||
<feOffset
|
||||
dx="0"
|
||||
dy="1"
|
||||
result="offset"
|
||||
id="feOffset3819" />
|
||||
<feComposite
|
||||
in="SourceGraphic"
|
||||
in2="offset"
|
||||
operator="over"
|
||||
result="composite2"
|
||||
id="feComposite3821" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="16"
|
||||
inkscape:cx="25.745257"
|
||||
inkscape:cy="9.618802"
|
||||
inkscape:document-units="px"
|
||||
inkscape:current-layer="layer1"
|
||||
showgrid="true"
|
||||
inkscape:window-width="989"
|
||||
inkscape:window-height="755"
|
||||
inkscape:window-x="22"
|
||||
inkscape:window-y="16"
|
||||
inkscape:window-maximized="0"
|
||||
showguides="true"
|
||||
inkscape:guide-bbox="true"
|
||||
guidecolor="#000000"
|
||||
guideopacity="0.49803922">
|
||||
<inkscape:grid
|
||||
type="xygrid"
|
||||
id="grid2985"
|
||||
empspacing="4"
|
||||
visible="true"
|
||||
enabled="true"
|
||||
snapvisiblegridlinesonly="true"
|
||||
spacingx="1px"
|
||||
spacingy="1px"
|
||||
originx="0px"
|
||||
originy="0px"
|
||||
color="#0000ff"
|
||||
opacity="0.03137255" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="20,26"
|
||||
id="guide3060" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="24,26"
|
||||
id="guide3062" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,22"
|
||||
id="guide3064" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="36,6"
|
||||
id="guide3066" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="26,0"
|
||||
id="guide3068" />
|
||||
<sodipodi:guide
|
||||
orientation="1,0"
|
||||
position="18,0"
|
||||
id="guide3070" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,10"
|
||||
id="guide3074" />
|
||||
<sodipodi:guide
|
||||
orientation="0,1"
|
||||
position="0,8"
|
||||
id="guide3076" />
|
||||
</sodipodi:namedview>
|
||||
<metadata
|
||||
id="metadata7">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer"
|
||||
transform="translate(0,-2)">
|
||||
<g
|
||||
id="g3759"
|
||||
style="fill:#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
|
@ -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
|
@ -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 |
|
@ -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
|
||||
|
|
11
build.gradle
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
public class CryptoFailedException extends Exception {
|
||||
public CryptoFailedException(Exception e){
|
||||
super(e);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
public class NoSessionsCreatedException extends Throwable{
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package eu.siacs.conversations.crypto.axolotl;
|
||||
|
||||
public interface OnMessageCreatedCallback {
|
||||
void run(XmppAxolotlMessage message);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
255
src/main/java/eu/siacs/conversations/ui/TrustKeysActivity.java
Normal 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));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
68
src/main/java/eu/siacs/conversations/ui/widget/Switch.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)) {
|
||||
|
|
144
src/main/java/eu/siacs/conversations/utils/FileUtils.java
Normal 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());
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package eu.siacs.conversations.xmpp;
|
||||
|
||||
public interface OnKeyStatusUpdated {
|
||||
public void onKeyStatusUpdated();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
BIN
src/main/res/drawable-hdpi/ic_action_done.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
src/main/res/drawable-hdpi/ic_done_black_24dp.png
Normal file
After Width: | Height: | Size: 177 B |
BIN
src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png
Normal file
After Width: | Height: | Size: 508 B |
Before Width: | Height: | Size: 294 B After Width: | Height: | Size: 294 B |
BIN
src/main/res/drawable-hdpi/ic_secure_indicator_white.png
Normal file
After Width: | Height: | Size: 322 B |
BIN
src/main/res/drawable-hdpi/message_bubble_received.9.png
Normal file
After Width: | Height: | Size: 765 B |
BIN
src/main/res/drawable-hdpi/message_bubble_received_warning.9.png
Normal file
After Width: | Height: | Size: 757 B |
BIN
src/main/res/drawable-hdpi/message_bubble_sent.9.png
Normal file
After Width: | Height: | Size: 687 B |
BIN
src/main/res/drawable-hdpi/switch_thumb_disable.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/main/res/drawable-hdpi/switch_thumb_off_normal.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
src/main/res/drawable-hdpi/switch_thumb_off_pressed.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/main/res/drawable-hdpi/switch_thumb_on_normal.png
Normal file
After Width: | Height: | Size: 1.9 KiB |