Merge branch 'development'

This commit is contained in:
iNPUTmice 2014-10-30 11:20:35 +01:00
commit f2c1a80059
82 changed files with 3093 additions and 1951 deletions

2
.gitmodules vendored
View file

@ -7,4 +7,4 @@
url = https://github.com/open-keychain/openpgp-api-lib.git
[submodule "libs/MemorizingTrustManager"]
path = libs/MemorizingTrustManager
url = https://github.com/ge0rg/MemorizingTrustManager
url = https://github.com/iNPUTmice/MemorizingTrustManager.git

View file

@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.siacs.conversations"
android:versionCode="31"
android:versionName="0.7.3" >
android:versionCode="32"
android:versionName="0.8-alpha" >
<uses-sdk
android:minSdkVersion="14"
@ -22,14 +23,10 @@
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
tools:replace="android:label"
android:theme="@style/ConversationsTheme" >
<service android:name="eu.siacs.conversations.services.XmppConnectionService" />
<provider
android:name="eu.siacs.conversations.services.ImageProvider"
android:authorities="eu.siacs.conversations.images"
android:exported="true" />
<receiver android:name="eu.siacs.conversations.services.EventReceiver" >
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />

325
README.md
View file

@ -1,19 +1,22 @@
#Conversations
Conversations - the very last word in instant messaging
# Conversations
Conversations: the very last word in instant messaging
[![Google Play](http://developer.android.com/images/brand/en_generic_rgb_wo_45.png)](https://play.google.com/store/apps/details?id=eu.siacs.conversations)
![screenshots](https://raw.githubusercontent.com/siacs/Conversations/master/screenshots.png)
##Design principles
## Design principles
* Be as beautiful and easy to use as possible without sacrificing security or
privacy
* Rely on existing, well established protocols (XMPP)
* Do not require a Google Account or specifically Google Cloud Messaging (GCM)
* Require as little permissons as possible
* Require as few permissions as possible
##Features
* End-to-end encryption with either OTR or openPGP
## Features
* End-to-end encryption with either [OTR](https://otr.cypherpunks.ca/) or [OpenPGP](http://www.openpgp.org/about_openpgp/)
* Sending and receiving images
* Indication when your contact has read your message
* Intuitive UI that follows Android Design guidelines
@ -21,47 +24,56 @@ Conversations - the very last word in instant messaging
* Syncs with desktop client
* Conferences (with support for bookmarks)
* Address book integration
* Multiple Accounts / unified inbox
* Multiple accounts / unified inbox
* Very low impact on battery life
###XMPP Features
Conversations works with every XMPP server out there. However XMPP is an extensible
protocol. These extensions are standardized as well in so called XEPs.
Conversations supports a couple of those to make the overall user experience better. There is a
chance that your current XMPP server does not 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 XEPs are - as of now:
* XEP-0065: SOCKS5 Bytestreams - or rather mod_proxy65. Will be used to transfer files if both parties are behind a firewall (NAT).
### XMPP Features
Conversations works with every XMPP server out there. However XMPP is an
extensible protocol. These extensions are standardized as well in so called
XEP's. Conversations supports a couple of these to make the overall user
experience better. There is a chance that your current XMPP server does not
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
files if both parties are behind a firewall (NAT).
* XEP-0138: Stream Compression saves bandwidth
* XEP-0163: Personal Eventing Protocol for avatars
* XEP-0198: Stream Management allows XMPP to survive small network outages and changes of the underlying TCP connection.
* XEP-0198: Stream Management 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
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-0352: Client State Indication let the server know whether or not
Conversations is in the background. Allows the server to save bandwidth by
withholding unimportent packages.
withholding unimportant packages.
## Team
#### Head of Development
##Team
####Head of Development
* [Daniel Gultsch](https://github.com/inputmice)
####Code Contributions
#### Code Contributions
(In order of appearance)
* [Rene Treffer](https://github.com/rtreffer)
* [Andreas Straub](https://github.com/strb)
* [Alethea Butler](https://github.com/alethea)
* [M. Dietrich](https://github.com/emdete)
* [betheg](https://github.com/betheg)
####Logo
#### Logo
* [Diego Turtulici](http://efesto.eigenlab.org/~diesys)
####Translations
#### Translations
* [Sergio Cárdenas](https://github.com/kruks23) (Spanish)
* [Benoit Bouvarel](https://github.com/BenoitBouvarel) (French)
* [Daniel Gultsch](https://github.com/iNPUTmice) (German)
@ -71,180 +83,215 @@ These XEPs are - as of now:
* [Anders Sandblad](https://github.com/andersruneson) (Swedish)
* [Aizaz AZ](http://www.linkedin.com/in/aizazhaider) (Chinese)
##FAQ
###General
####How do I install Conversations?
## FAQ
### General
#### How do I install Conversations?
Conversations is entirely open source and licensed under GPLv3. So if you are a
software developer you can check out the sources from github and use ant to
software developer you can check out the sources from GitHub and use ant to
build your apk file.
The more convenient way - which not only gives you automatic updates but also
supports the further development of Conversations - is to buy the App in the Google
[Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations).
####I don't have a Google Account but I would still like to make a contribution
I accept donations over PayPal, BitCoin and Flattr. For donations via PayPal you can use the email address donate@siacs.eu or the button below.
The more convenient way — which not only gives you automatic updates but also
supports the further development of Conversations - is to buy the App in the
Google [Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations).
#### I don't have a Google Account but I would still like to make a contribution
I accept donations over PayPal, Bitcoin and Flattr. For donations via PayPal you
can use the email address `donate@siacs.eu` or the button below.
[![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CW3SYT3KG5PDL)
**Disclaimer:** I'm not a huge fan of PayPal and their business policies. For larger
contributions please get in touch with me beforehand and we can talk about bank
transfer (SEPA).
**Disclaimer:** I'm not a huge fan of PayPal and their business policies. For
larger contributions please get in touch with me beforehand and we can talk
about bank transfer (SEPA).
My Bitcoin Address is: 1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu
My Bitcoin Address is: `1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu`
[![Flattr this!](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=inputmice&url=https%3A%2F%2Fgithub.com%2Fsiacs%2FConversations)
[![Flattr this!](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=inputmice&url=http%3A%2F%2Fconversations.siacs.eu&title=Conversations&tags=github&category=software)
####How do I create an account?
XMPP like email for example is a federated protocol which means that there is
not one company you can create your 'official xmpp account' with but there are
hundreds or even thousands of provider out there. To find one use a web search
engine of your choice. Or maybe your university has one. Or you can run your own.
Or ask a friend to run one. Once you found one you can use Conversations to
create an account. Just select 'register new account on server' within the
create account dialog.
#### How do I create an account?
####Conversations doesn't work for me. Where can I get help?
You can join our conference room on conversations@conference.siacs.eu A lot of
XMPP, like email, is a federated protocol which means that there is not one
company you can create an 'official XMPP account' with. Instead there are
hundreds, or even thousands, of provider out there. To find one use a web search
engine of your choice. Or maybe your university has one. Or you can run your
own. Or ask a friend to run one. Once you've found one, you can use
Conversations to create an account. Just select 'register new account on server'
within the create account dialog.
#### Conversations doesn't work for me. Where can I get help?
You can join our conference room on `conversations@conference.siacs.eu` A lot of
people in there are able to answer basic questions about the usage of
Conversations or can provide you with tips on running your own XMPP server. If
you found a bug or your app crashes please read the Developer / Report Bugs
section of this document.
####I need professional support with Conversations or setting up my server
I'm available for hire. Contact me at inputmice@siacs.eu
#### I need professional support with Conversations or setting up my server
####How does the address book integration work?
The address bock integration was designed to protect your privacy. Conversations
I'm available for hire. Contact me at `inputmice@siacs.eu`.
#### How does the address book integration work?
The address book integration was designed to protect your privacy. Conversations
neither uploads contacts from your address book to your server nor fills your
address book with unnecessary contacts from your online roster. If you manually
add a Jabber ID to your phones address book Conversations will use the name and
the profile picture of this contact. To make the process of adding Jabber IDs to
your address book easier you can click on the profile picture in the contact
details within Conversations. This will start an add to address book intent with the jabber ID
as payload. This doesnt require Conversations to have write permissions on your
address book but also doesnt require you to copy past Jabber ID from one app to
another.
details within Conversations. This will start an "add to address book" intent
with the JID as the payload. This doesn't require Conversations to have write
permissions on your address book but also doesn't require you to copy/paste a
JID from one app to another.
####I get 'delivery failed' on my messages
If you get delivery failed on images its probably because the recipient lost
network connectivity during recepiton. In that case you can try it again at a
#### I get 'delivery failed' on my messages
If you get delivery failed on images it's probably because the recipient lost
network connectivity during reception. In that case you can try it again at a
later time.
For text messages the answer to your question is a little bit more complex.
'delivery failed' on text messages is always something that is being reported by
the server. The most common reason for this is that the recipient failed to
resume a connection. When a client loses connectivity for a short time the client
usually has a five minute window to pick up that connection again. When the
client fails to do so because the network connectivity is out for longer than
that all messages sent to that client will be returned to the sender resulting
in a delivery failed.
When you see 'delivery failed' on text messages, it is always something that is
being reported by the server. The most common reason for this is that the
recipient failed to resume a connection. When a client loses connectivity for a
short time the client usually has a five minute window to pick up that
connection again. When the client fails to do so because the network
connectivity is out for longer than that all messages sent to that client will
be returned to the sender resulting in a delivery failed.
Other less common reasons are that the message you sent didnt meet some
criterias enforced by the server. (Too large, too many) Another reason could be
that the recipient is offline and the server doesnt provide offline storage.
Other less common reasons are that the message you sent didn't meet some
criteria enforced by the server (too large, too many). Another reason could be
that the recipient is offline and the server doesn't provide offline storage.
Usually you are able to distinguish between these two groups in the fact that
the first one happens always after some time and the second one happens almost
instantly.
####Where can I see the status of my contacts? How can I set a status or priority
Status are a horrible metric. Setting them manually to a proper value rarely
#### Where can I see the status of my contacts? How can I set a status or priority?
Statuses are a horrible metric. Setting them manually to a proper value rarely
works because users are either lazy or just forget about them. Setting them
automatically does not provide quality results either. Keyboard or mouse
activity as indicator for example fails when the user is just looking at
something (reading an article, watching a movie). Furthermore automatic setting
of status always implies an impact on your privacy. (Are you sure you want
of status always implies an impact on your privacy (are you sure you want
everybody in your contact list to know that you have been using your computer at
4am?!)
4am‽).
In the past status has been used to judge the likelihood of whether or not your
messages are being read. This is no longer necessary. With Chat Markers
(XEP-0333, supported by Conversations since 0.4) we have the ability to **know**
whether or not your messages are being read.
Similar things can be said for priorities. In the past priorities have been used
(By servers, not by clients!) to route your messages to one specific client.
With carbon messages (XEP-0280, supported by Conversations since 0.1) this is no
longer necessary. Using priorities to route OTR messages isn't pratical either
because they are not changeable on the fly. Metrics like last active client
(the client which sent the last message) are much better.
whether or not your messages are being read. Similar things can be said for
priorities. In the past priorities have been used (by servers, not by clients!)
to route your messages to one specific client. With carbon messages (XEP-0280,
supported by Conversations since 0.1) this is no longer necessary. Using
priorities to route OTR messages isn't practical either because they are not
changeable on the fly. Metrics like last active client (the client which sent
the last message) are much better.
Unfortunately these modern replacements for legacy XMPP features are not widely
adopted. However Conversations should be an instant messenger for the future and
instead of making Conversations compatible with the past we should work on
implementing new, improved technologies into other XMPP clients as well.
implementing new, improved technologies and getting them into other XMPP clients
as well.
Making these status and priority optional isn't a solution either because
Conversations is trying to get rid of old behaviours and set an example for
other clients.
####Conversations is missing a certain feature
I'm open for new feature suggestions. You can use the issue tracker on github.
Please take some time to browse through the issues to see if someone else
already suggested it. Be assured that I read each and every ticket. If I like it
I will leave it open until it's implemented. If I don't like it I will close
it. (Usually with a short comment). If I don't comment on an feature request
that's probably a good sign because this means I agree with you. Commenting with
+1 on either open or closed issues won't change my mind nor will it accelerate the
development.
#### Conversations is missing a certain feature
I'm open for new feature suggestions. You can use the [issue tracker][issues] on
GitHub. Please take some time to browse through the issues to see if someone
else already suggested it. Be assured that I read each and every ticket. If I
like it I will leave it open until it's implemented. If I don't like it I will
close it (usually with a short comment). If I don't comment on an feature
request that's probably a good sign because this means I agree with you.
Commenting with +1 on either open or closed issues won't change my mind, nor
will it accelerate the development.
#### You closed my feature request but I want it really really badly
####You closed my feature request but I want it really really badly
Just write it yourself and send me a pull request. If I like it I will happily
merge it if I don't at least you and like minded people get to enjoy it.
####I need a feature and I need it now!
I am available for hire. Contact me JID: inputmice@siacs.eu
#### I need a feature and I need it now!
###Security
####Why are there two end-to-end encryption methods and which one should I choose?
In most cases OTR should be the encryption method of choice. It works out of the box with most contacts as long as they are online.
However PGP can be in some cases (carbonated messages to multiple clients) be
more flexible.
####How do I use openPGP
Before you continue reading you should notice that the openPGP support in
Conversations is marked as experimental. This is not because it will make the app
unstable but because the fundamental concepts of PGP aren't ready for a
widespread use. The way PGP works is that you trust Key IDs instead of XMPP- or email addresses. So in theory your contact list should consist of Public-Key-IDs instead of email addresses. But of course no email or xmpp client out there implements these concepts. Plus PGP in the context of instant messaging has a couple of downsides. It is vulnerable to replay attacks, it is rather verbose, and decrypting and encrypting takes longer than OTR. It is however asynchronous and works well with carbonated messages.
I am available for hire. Contact me via XMPP: `inputmice@siacs.eu`
To use openpgp you have to install the opensource app OpenKeychain (www.openkeychain.org) and then long press on the account in manage accounts and choose renew PGP announcement from the contextual menu.
####How does the encryption for conferences work?
For conferences the only supported encryption method is OpenPGP. (OTR does not
work with multiple participants.) Every participant has to announce their
OpenPGP key. (See answer above). If you would like to send encrypted messages to
### Security
#### Why are there two end-to-end encryption methods and which one should I choose?
In most cases OTR should be the encryption method of choice. It works out of the
box with most contacts as long as they are online. However PGP can, in some
cases, (message carbons to multiple clients) be more flexible.
#### How do I use OpenPGP
Before you continue reading you should note that the OpenPGP support in
Conversations is experimental. This is not because it will make the app unstable
but because the fundamental concepts of PGP aren't ready for widespread use.
The way PGP works is that you trust Key IDs instead of JID's or email addresses.
So in theory your contact list should consist of Public-Key-IDs instead of
JID's. But of course no email or XMPP client out there implements these
concepts. Plus PGP in the context of instant messaging has a couple of
downsides: It is vulnerable to replay attacks, it is rather verbose, and
decrypting and encrypting takes longer than OTR. It is however asynchronous and
works well with message carbons.
To use OpenPGP you have to install the open source app
[OpenKeychain](www.openkeychain.org) and then long press on the account in
manage accounts and choose renew PGP announcement from the contextual menu.
#### How does the encryption for conferences work?
For conferences the only supported encryption method is OpenPGP (OTR does not
work with multiple participants). Every participant has to announce their
OpenPGP key (see answer above). If you would like to send encrypted messages to
a conference you have to make sure that you have every participant's public key
in your OpenKeychain. Right now there is no check in Conversations to ensure
that. You have to take care of that yourself. Go to the conference details and
touch every key id (The hexadecimal number below a contact). This will send you
to OpenKeychain which will assist you on adding the key.
This works best in very small conferences with contacts you are already using
OpenPGP with. This feature is regarded experimental. Conversations is the only
client that uses XEP-0027 with conferences. (The XEP neither specifically allows
nor disallows this.)
###Development
####How do I build Conversations
to OpenKeychain which will assist you on adding the key. This works best in
very small conferences with contacts you are already using OpenPGP with. This
feature is regarded experimental. Conversations is the only client that uses
XEP-0027 with conferences. (The XEP neither specifically allows nor disallows
this.)
### Development
#### How do I build Conversations
Make sure to have ANDROID_HOME point to your Android SDK
```
git clone https://github.com/siacs/Conversations.git
cd Conversations
git submodule update --init --recursive
ant clean
ant debug
```
####How do I debug Conversations
git clone https://github.com/siacs/Conversations.git
cd Conversations
git submodule update --init --recursive
ant clean
ant debug
#### How do I debug Conversations
If something goes wrong Conversations usually exposes very little information in
the UI. (Other than the fact that something didn't work)
However with adb (android debug bridge) you squeeze some more information out of
Conversations. These information are especially useful if you are experiencing
troubles with your connection or with file transfer.
````
adb -d logcat -v time -s conversations
````
####I found a bug
Please report it to our issue tracker. If your app crashes please provide a
stack trace. If you are experiencing missbehaviour please provide detailed
steps to reproduce.
Always mention whether you are running the latest Play Store version or the
current HEAD.
If you are having problems connecting to your XMPP server your file transfer
doesnt work as expected please always include a logcat debug output with your
issue. (See above)
the UI (other than the fact that something didn't work). However with adb
(android debug bridge) you squeeze some more information out of Conversations.
These information are especially useful if you are experiencing trouble with
your connection or with file transfer.
adb -d logcat -v time -s conversations
#### I found a bug
Please report it to our [issue tracker][issues]. If your app crashes please
provide a stack trace. If you are experiencing misbehaviour please provide
detailed steps to reproduce. Always mention whether you are running the latest
Play Store version or the current HEAD. If you are having problems connecting to
your XMPP server your file transfer doesnt work as expected please always
include a logcat debug output with your issue (see above).
[issues]: https://github.com/siacs/Conversations/issues

View file

@ -1,108 +0,0 @@
<?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="128"
height="128"
id="svg4066"
version="1.1"
inkscape:version="0.48.5 r10040"
sodipodi:docname="ic_action_copy.svg">
<defs
id="defs4068" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.54117647"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.67"
inkscape:cx="51.750573"
inkscape:cy="57.547291"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
borderlayer="false"
inkscape:window-width="1035"
inkscape:window-height="853"
inkscape:window-x="369"
inkscape:window-y="3"
inkscape:window-maximized="0" />
<metadata
id="metadata4071">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-924.36218)">
<rect
ry="0"
height="91.708199"
width="71.625328"
stroke-miterlimit="4"
y="952.36743"
x="42.730034"
id="rect10"
style="fill:none;stroke:#000000;stroke-width:8.6679945;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:0.54117647;stroke-dasharray:none"
inkscape:transform-center-x="-21.391573"
inkscape:transform-center-y="28.294015" />
<path
style="fill:#000000;fill-opacity:0.5411765;fill-rule:evenodd;stroke:#000000;stroke-width:0.41999999999999998;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117649999999995;stroke-miterlimit:4;stroke-dasharray:none"
d="m 20.552281,933.36985 0,0.0209 -0.128276,0 -0.399078,99.83215 0.213792,0 0,0.1463 13.212333,-0.021 0.05701,-8.1392 -4.076297,0.011 0.327814,-84.87039 58.436429,0 0.0285,3.427 11.10293,0 -0.0855,-9.25707 -0.057,0 0,-1.1493 -78.63263,0 z"
id="rect12-6"
inkscape:connector-curvature="0" />
<rect
height="4.7259107"
width="37.242958"
y="967.49921"
x="50.137043"
id="rect12"
style="fill:#000000;fill-opacity:0.54117647;fill-rule:evenodd;stroke:#000000;stroke-width:0.23799089px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647" />
<rect
style="fill:#000000;fill-opacity:0.54117647000000002;fill-rule:evenodd;stroke:#000000;stroke-width:0.274;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647000000002;stroke-miterlimit:4;stroke-dasharray:none"
id="rect4272"
x="50.137043"
y="982.49921"
width="49.452484"
height="4.7259107" />
<rect
height="4.7259107"
width="43.542446"
y="997.49921"
x="50.137043"
id="rect4274"
style="fill:#000000;fill-opacity:0.54117647;fill-rule:evenodd;stroke:#000000;stroke-width:0.2573325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647" />
<rect
style="fill:#000000;fill-opacity:0.54117647;fill-rule:evenodd;stroke:#000000;stroke-width:0.25050664px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647"
id="rect4276"
x="50.137043"
y="1012.4992"
width="41.263123"
height="4.7259107" />
<rect
height="4.7259107"
width="49.397911"
y="1027.4993"
x="50.137043"
id="rect4278"
style="fill:#000000;fill-opacity:0.54117647;fill-rule:evenodd;stroke:#000000;stroke-width:0.27408957px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0.54117647" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

BIN
art/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View file

@ -1,6 +1,16 @@
#!/bin/env ruby
resolutions={'mdpi'=> 1, 'hdpi' => 1.5, 'xhdpi' => 2, 'xxhdpi' => 3}
images = { 'conversations.svg' => ['ic_launcher', 48], 'conversations_baloon.svg' => ['ic_activity', 32], 'conversations_mono.svg' => ['ic_notification', 24], 'ic_received_indicator.svg' => ['ic_received_indicator', 12], 'ic_action_copy.svg' => ['ic_action_copy', 32] }
resolutions={
'mdpi'=> 1,
'hdpi' => 1.5,
'xhdpi' => 2,
'xxhdpi' => 3,
}
images = {
'conversations.svg' => ['ic_launcher', 48],
'conversations_baloon.svg' => ['ic_activity', 32],
'conversations_mono.svg' => ['ic_notification', 24],
'ic_received_indicator.svg' => ['ic_received_indicator', 12],
}
images.each do |source, result|
resolutions.each do |name, factor|
size = factor * result[1]

@ -1 +1 @@
Subproject commit 452f70208f0dd5f9e56376944e96f5c10704245e
Subproject commit fad835037adc1bd313bb56b694426fca4eb67346

Binary file not shown.

Before

Width:  |  Height:  |  Size: 717 B

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 470 B

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" >
<solid android:color="@color/primarybackground" />
<corners android:radius="2dp" />
<stroke
android:width="0.5dp"
android:color="@color/divider" >
</stroke>
<padding
android:bottom="0dp"
android:left="0dp"
android:right="0dp"
android:top="0dp" />
</shape>

View file

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle" >
<size
android:height="1.5dp"
android:width="2000dp" />
<solid android:color="@color/divider" />
</shape>

View file

@ -1,126 +1,112 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primarybackground" >
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@color/secondarybackground" >
<LinearLayout
android:layout_width="match_parent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
style="@style/sectionHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/action_contact_details"
android:textColor="@color/primarytext" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:minHeight="88dp"
android:padding="8dp" >
android:layout_margin="8dp"
android:background="@drawable/infocard_border"
android:padding="16dp" >
<QuickContactBadge
android:id="@+id/details_contact_badge"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_centerVertical="true"
android:layout_alignParentTop="true"
android:scaleType="centerCrop" />
<LinearLayout
android:id="@+id/details_jidbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="16dp"
android:layout_toRightOf="@+id/details_contact_badge"
android:orientation="vertical"
android:paddingLeft="8dp" >
android:orientation="vertical" >
<TextView
android:id="@+id/details_contactjid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:singleLine="true"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
android:id="@+id/details_contactstatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:text="@string/account_settings_example_jabber_id"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold" />
<TextView
android:id="@+id/details_lastseen"
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:singleLine="true"
android:orientation="horizontal" >
<TextView
android:id="@+id/details_contactstatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=" · "
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
android:id="@+id/details_lastseen"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeBody" />
</LinearLayout>
<CheckBox
android:id="@+id/details_send_presence"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/send_presence_updates"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<CheckBox
android:id="@+id/details_receive_presence"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/receive_presence_updates"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
</LinearLayout>
<TextView
android:id="@+id/details_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_below="@+id/details_jidbox"
android:layout_marginTop="32dp"
android:text="@string/using_account"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeInfo" />
</RelativeLayout>
<TextView
style="@style/sectionHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/your_account"
android:textColor="@color/primarytext" />
<TextView
android:id="@+id/details_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
style="@style/sectionHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/subscriptions"
android:textColor="@color/primarytext" />
<CheckBox
android:id="@+id/details_send_presence"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/send_presence_updates"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<CheckBox
android:id="@+id/details_receive_presence"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/receive_presence_updates"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
style="@style/sectionHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/keys" />
<LinearLayout
android:id="@+id/details_contact_keys"
android:layout_width="match_parent"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/infocard_border"
android:divider="?android:dividerHorizontal"
android:orientation="vertical"
android:padding="8dp"
android:showDividers="middle" >
</LinearLayout>
</LinearLayout>

View file

@ -2,7 +2,7 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primarybackground" >
android:background="@color/secondarybackground" >
<ScrollView
android:layout_width="fill_parent"
@ -19,8 +19,10 @@
android:id="@+id/editor"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/infocard_border"
android:orientation="vertical"
android:padding="8dp" >
android:padding="16dp" >
<TextView
android:layout_width="wrap_content"
@ -34,7 +36,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/account_settings_example_jabber_id"
android:inputType="textEmailAddress" />
android:inputType="textEmailAddress"
android:textColor="@color/primarytext"
android:textColorHint="@color/secondarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
android:layout_width="wrap_content"
@ -49,7 +54,10 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:inputType="textPassword" />
android:inputType="textPassword"
android:textColor="@color/primarytext"
android:textColorHint="@color/secondarytext"
android:textSize="?attr/TextSizeBody" />
<CheckBox
android:id="@+id/account_register_new"
@ -76,31 +84,25 @@
android:layout_marginTop="8dp"
android:hint="@string/confirm_password"
android:inputType="textPassword"
android:visibility="gone" />
android:visibility="gone"
android:textColor="@color/primarytext"
android:textColorHint="@color/secondarytext"
android:textSize="?attr/TextSizeBody" />
</LinearLayout>
<LinearLayout
<LinearLayout
android:id="@+id/stats"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_marginTop="8dp"
android:layout_margin="8dp"
android:background="@drawable/infocard_border"
android:orientation="vertical"
android:padding="16dp"
android:visibility="gone" >
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="@string/additional_information"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold" />
<TableLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:stretchColumns="1" >
<TableRow
@ -110,13 +112,17 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_info_session_established" />
android:text="@string/server_info_session_established"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
android:id="@+id/session_est"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right" />
android:layout_gravity="right"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
</TableRow>
<TableRow
@ -127,13 +133,16 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_info_pep"
android:textColor="@color/primarytext" />
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
android:id="@+id/server_info_pep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right" />
android:layout_gravity="right"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
</TableRow>
<TableRow
@ -143,13 +152,17 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_info_stream_management" />
android:text="@string/server_info_stream_management"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
android:id="@+id/server_info_sm"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right" />
android:layout_gravity="right"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
</TableRow>
<TableRow
@ -159,59 +172,64 @@
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/server_info_carbon_messages" />
android:text="@string/server_info_carbon_messages"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
android:id="@+id/server_info_carbons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right" />
android:layout_gravity="right"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
</TableRow>
</TableLayout>
<TextView
android:id="@+id/otr_fingerprint_headline"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:gravity="center_horizontal"
android:text="@string/otr_fingerprint"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold" />
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginTop="8dp">
android:id="@+id/otr_fingerprint_box"
android:layout_marginTop="32dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_toLeftOf="@+id/action_copy_to_clipboard"
android:orientation="vertical"
android:layout_centerVertical="true">
android:orientation="vertical">
<TextView
android:id="@+id/otr_fingerprint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody"
android:typeface="monospace" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeInfo"
android:text="@string/otr_fingerprint"/>
</LinearLayout>
<ImageButton
android:id="@+id/action_copy_to_clipboard"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?android:selectableItemBackground"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:padding="4dp"
android:scaleType="fitXY"
android:background="?android:selectableItemBackground"
android:padding="8dp"
android:src="@drawable/ic_action_copy"
android:visibility="invisible" />
android:visibility="visible" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View file

@ -1,46 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/primarybackground" >
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@color/secondarybackground" >
<LinearLayout
android:layout_width="wrap_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
style="@style/sectionHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/muc_details_conference" />
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/infocard_border"
android:orientation="vertical"
android:padding="16dp" >
<TextView
android:id="@+id/muc_jabberid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:singleLine="true"
android:text="@string/account_settings_example_jabber_id"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" />
<TextView
style="@style/sectionHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:text="@string/you" />
android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<RelativeLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/activatedBackgroundIndicator"
android:padding="8dp"
android:paddingBottom="8dp" >
android:layout_height="wrap_content">
<ImageView
android:id="@+id/your_photo"
@ -85,22 +74,26 @@
android:padding="8dp"
android:src="@drawable/ic_action_edit_dark" />
</RelativeLayout>
<LinearLayout
android:id="@+id/muc_more_details"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<TextView
android:id="@+id/muc_participants_header"
style="@style/sectionHeader"
<TextView
android:id="@+id/details_account"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="8dp"
android:text="@string/muc_details_other_members" />
android:layout_gravity="right"
android:layout_marginTop="32dp"
android:text="@string/using_account"
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeInfo" />
</LinearLayout>
<LinearLayout
android:id="@+id/muc_more_details"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/infocard_border"
android:orientation="vertical"
android:padding="8dp" >
<LinearLayout
android:id="@+id/muc_members"
@ -111,7 +104,6 @@
android:orientation="vertical"
android:showDividers="middle" >
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/invite"
@ -123,4 +115,5 @@
android:text="@string/invite_contact" />
</LinearLayout>
</LinearLayout>
</ScrollView>

View file

@ -16,14 +16,15 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeHeadline"
android:textSize="?attr/TextSizeBody"
android:typeface="monospace" />
<TextView
android:id="@+id/key_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/secondarytext" />
android:textColor="@color/secondarytext"
android:textSize="?attr/TextSizeInfo"/>
</LinearLayout>
<ImageButton

View file

@ -15,7 +15,8 @@
android:layout_alignParentBottom="true"
android:layout_toRightOf="@+id/message_photo"
android:background="@drawable/message_border"
android:minHeight="48dp" >
android:minHeight="48dp"
android:longClickable="true">
<LinearLayout
android:layout_width="wrap_content"
@ -43,7 +44,6 @@
android:layout_height="wrap_content"
android:autoLink="web"
android:textColor="@color/primarytext"
android:textIsSelectable="true"
android:textSize="?attr/TextSizeBody" />
<Button

View file

@ -15,7 +15,8 @@
android:layout_alignParentBottom="true"
android:layout_toLeftOf="@+id/message_photo"
android:background="@drawable/message_border"
android:minHeight="48dp" >
android:minHeight="48dp"
android:longClickable="true">
<LinearLayout
android:layout_width="wrap_content"
@ -43,9 +44,16 @@
android:layout_height="wrap_content"
android:autoLink="web"
android:textColor="@color/primarytext"
android:textIsSelectable="true"
android:textSize="?attr/TextSizeBody" />
<Button
android:id="@+id/download_button"
style="?android:attr/buttonStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/download_image"
android:visibility="gone" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item
android:id="@+id/copy_text"
android:title="@string/copy_text"/>
<item
android:id="@+id/share_image"
android:title="@string/share_image"/>
<item
android:id="@+id/copy_url"
android:title="@string/copy_original_url"/>
<item
android:id="@+id/send_again"
android:title="@string/send_again"/>
<item
android:id="@+id/download_image"
android:title="@string/download_image"/>
</menu>

View file

@ -22,7 +22,7 @@
</string-array>
<string-array name="mute_options_descriptions">
<item>30 Minuten</item>
<item>eine Stunde</item>
<item>1 Stunde</item>
<item>2 Stunden</item>
<item>8 Stunden</item>
<item>bis auf Widerruf</item>

View file

@ -51,7 +51,7 @@
<string name="send_now">Jetzt abschicken</string>
<string name="send_never">Nie mehr nachfragen</string>
<string name="problem_connecting_to_account">Es gibt Probleme beim Verbindungsaufbau mit einem Konto</string>
<string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Konto</string>
<string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Konten</string>
<string name="touch_to_fix">Drücke hier, um das Konto zu verwalten</string>
<string name="attach_file">Datei anfügen</string>
<string name="not_in_roster">Der Kontakt ist nicht in deiner Kontaktliste. Möchtest du ihn hinzufügen?</string>
@ -65,7 +65,7 @@
<string name="clear_histor_msg">Möchtest du alle Nachrichten in dieser Unterhaltung löschen?\n\n<b>Achtung:</b> Dies beeinflusst nicht Nachrichten, die auf anderen Geräten oder Servern gespeichert sind.</string>
<string name="delete_messages">Nachrichten löschen</string>
<string name="also_end_conversation">Diese Unterhaltung danach beenden</string>
<string name="choose_presence">Choose presence to contact</string>
<string name="choose_presence">Ressource des Kontakts auswählen</string>
<string name="send_plain_text_message">Unverschlüsselt schreiben</string>
<string name="send_otr_message">OTR-verschlüsselt schreiben</string>
<string name="send_pgp_message">OpenPGP-verschlüsselt schreiben</string>
@ -81,9 +81,9 @@
<string name="offering">angeboten&#8230;</string>
<string name="waiting">warten&#8230;</string>
<string name="no_pgp_key">Kein OpenPGP-Schlüssel gefunden</string>
<string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string>
<string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge OpenPGP einrichten.</small></string>
<string name="no_pgp_keys">Keine OpenPGP-Schlüssel gefunden</string>
<string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string>
<string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil deine Kontakte ihre Schlüssel nicht preisgeben.\n\n<small>Bitte sag deinen Kontakten, sie mögen OpenPGP einrichten.</small></string>
<string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier, um sie anzuzeigen und zu entschlüsseln.</i></string>
<string name="encrypted_image_received"><i>Verschlüsseltes Bild erhalten. Drücke hier, um es anzuzeigen und zu entschlüsseln.</i></string>
<string name="image_file"><i>Bild erhalten. Drücke hier, um es anzuzeigen.</i></string>
@ -100,7 +100,7 @@
<string name="pref_sound">Klingelton</string>
<string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</string>
<string name="pref_conference_notifications">Konferenz-Benachrichtigungen</string>
<string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde.</string>
<string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde</string>
<string name="pref_notification_grace_period">Gnadenfrist</string>
<string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.</string>
<string name="pref_advanced_options">Erweiterte Optionen</string>
@ -233,7 +233,7 @@
<string name="skip">Überspringen</string>
<string name="pref_ui_options">Benutzeroberfläche</string>
<string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string>
<string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte das dies nicht unbedingt in allen Fällen funktioniert.</string>
<string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string>
<string name="disable_notifications">Benachrichtigungen deaktivieren</string>
<string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string>
<string name="notifications_disabled">Benachrichtigungen sind deaktiviert</string>
@ -252,9 +252,11 @@
<string name="pref_force_encryption_summary">Nachrichten immer verschlüsseln (außer für Konferenzen)</string>
<string name="pref_dont_save_encrypted">Verschlüsselte Nachrichten nicht speichern</string>
<string name="pref_dont_save_encrypted_summary">Achtung: Kann zu Nachrichtenverlust führen</string>
<string name="pref_enable_legacy_ssl">Alte SSL-Version aktivieren</string>
<string name="pref_enable_legacy_ssl_summary">Aktiviert SSLv3-Unterstützung für alte Server. Achtung: SSLv3 ist unsicher.</string>
<string name="pref_expert_options">Einstellungen für Experten</string>
<string name="pref_expert_options_summary">Hier bitte vorsichtig sein</string>
<string name="pref_use_larger_font">Schriftgröße erhöhen</string>
<string name="pref_use_larger_font">Schrift vergrößern</string>
<string name="pref_use_larger_font_summary">Überall in der App eine größere Schrift verwenden</string>
<string name="pref_use_send_button_to_indicate_status">Absende-Knopf zeigt Online-Status an</string>
<string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string>
@ -265,5 +267,19 @@
<string name="conference_banned">Du wurdest aus dem Konferenzraum verbannt</string>
<string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</string>
<string name="conference_kicked">Du wurdest aus dem Konferenzraum geworfen</string>
<string name="using_account">Verwende Konto %s</string>
<string name="checking_image">Prüfe Bild auf HTTP-Host</string>
<string name="image_file_deleted">Bilddatei wurde gelöscht</string>
<string name="not_connected_try_again">Nicht verbunden, bitte später versuchen</string>
<string name="check_image_filesize">Bildgröße prüfen</string>
<string name="message_options">Nachrichtenoptionen</string>
<string name="copy_text">Text kopieren</string>
<string name="share_image">Bild teilen</string>
<string name="copy_original_url">Original-URL kopieren</string>
<string name="send_again">Erneut senden</string>
<string name="image_url">Bild-URL</string>
<string name="message_text">Nachrichtentext</string>
<string name="url_copied_to_clipboard">URL in Zwischenablage kopiert</string>
<string name="message_copied_to_clipboard">Nachricht in Zwischenablage kopiert</string>
</resources>

View file

@ -250,6 +250,8 @@
<string name="pref_force_encryption_summary">Siempre enviar mensajes encriptados (excepto para conferencias)</string>
<string name="pref_dont_save_encrypted">No guardar mensajes encriptados</string>
<string name="pref_dont_save_encrypted_summary">Aviso: Esto podría llevar a pérdida de mensajes</string>
<string name="pref_enable_legacy_ssl">Habilitar SSL heredado</string>
<string name="pref_enable_legacy_ssl_summary">Habilita soporte SSLv3 para servidores heredados. Advertencia: SSLv3 se considera no seguro.</string>
<string name="pref_expert_options">Ajustes avanzados</string>
<string name="pref_expert_options_summary">Por favor, cuidado con estas opciones</string>
<string name="pref_use_larger_font">Incrementar tamaño de fuente</string>
@ -265,5 +267,19 @@
<string name="conference_banned">Tu entrada a esta conferencia ha sido prohibida</string>
<string name="conference_members_only">Esta conferencia es solo para miembros</string>
<string name="conference_kicked">Has sido expulsado de esta conferencia</string>
<string name="using_account">Usando cuenta %s</string>
<string name="checking_image">Comprobando imagen en servidor HTTP</string>
<string name="image_file_deleted">El archivo de imagen ha sido eliminado</string>
<string name="not_connected_try_again">No estás concectado. Inténtalo más tarde</string>
<string name="check_image_filesize">Comprobar el tamaño del archivo de imagen</string>
<string name="message_options">Opciones de mensaje</string>
<string name="copy_text">Copiar texto</string>
<string name="share_image">Compartir imagen</string>
<string name="copy_original_url">Copiar URL original</string>
<string name="send_again">Volver a enviar</string>
<string name="image_url">URL Imagen</string>
<string name="message_text">Mensaje de texto</string>
<string name="url_copied_to_clipboard">URL copiada al portapapeles</string>
<string name="message_copied_to_clipboard">Mensaje copiado al portapapeles</string>
</resources>

View file

@ -205,7 +205,7 @@
<string name="conference_address">Konferentziaren helbidea</string>
<string name="conference_address_example">gela@conference.example.com</string>
<string name="save_as_bookmark">Gorde laster-marka bezala</string>
<string name="delete_bookmark">Laster-marka ezbatu</string>
<string name="delete_bookmark">Laster-marka ezabatu</string>
<string name="bookmark_already_exists">Laster-marka hau existitzen da dagoeneko</string>
<string name="you">Zu</string>
<string name="action_edit_subject">Konferentziaren gaia editatu</string>
@ -250,11 +250,27 @@
<string name="pref_force_encryption_summary">Mezuak beti enkriptatuta bidali (konferentzietan izan ezik)</string>
<string name="pref_dont_save_encrypted">Ez gorde enkriptatutako mezuak</string>
<string name="pref_dont_save_encrypted_summary">Adi: Honek mezuen galera ekar lezake</string>
<string name="pref_enable_legacy_ssl">Oinordetutako SSL gaitu</string>
<string name="pref_enable_legacy_ssl_summary">SSLv3 gaitzen du oinordetutako zerbitzarietarako. Adi: SSLv3 ez segurutzat hartzen da.</string>
<string name="pref_expert_options">Adituentzako aukerak</string>
<string name="pref_expert_options_summary">Mesedez kontuz ibili hauekin</string>
<string name="pref_use_larger_font">Letraren tamaina handitu</string>
<string name="pref_use_larger_font_summary">Letra tamaina handiagoa erabili aplikazio osoan zehar</string>
<string name="pref_use_send_button_to_indicate_status">Bidaltze botoiak egoera adierazten du</string>
<string name="pref_use_indicate_received">Mezuen jasotzea eskatu</string>
<string name="pref_use_indicate_received_summary">Jasotako mezuak marka berde batekin markatuko dira. Baliteke kasu guztietan ez funtzionatzea.</string>
<string name="pref_use_send_button_to_indicate_status_summary">Bidaltze botoia koloreztatu kontaktu baten egoera adierazteko</string>
<string name="pref_expert_options_other">Besteak</string>
<string name="pref_conference_name">Konferentziaren izena</string>
<string name="pref_conference_name_summary">Erabili gelaren gaia konferentziak identifikatzeko eta ez JIDa</string>
<string name="toast_message_otr_fingerprint">OTR hatz-marka arbelara kopiatu da</string>
<string name="conference_banned">Konferentzia honetara sartzea debekatuta duzu</string>
<string name="conference_members_only">Konferentzia hau kideentzat da soilik</string>
<string name="conference_kicked">Konferentzia honetatik kanporatua izan zara</string>
<string name="using_account">%s kontua erabiltzen</string>
<string name="checking_image">Irudia egiaztatzen HTTP ostalarian</string>
<string name="image_file_deleted">Irudia ezabatu egin da</string>
<string name="not_connected_try_again">Ez zaude konektatuta. Saiatu beranduago berriz</string>
<string name="check_image_filesize">Irudiaren tamaina egiaztatu</string>
</resources>

View file

@ -87,6 +87,7 @@
<string name="encrypted_message_received"><i>Message chiffré reçu. Appuyez pour le déchiffrer.</i></string>
<string name="encrypted_image_received"><i>Image chiffrée reçue. Appuyez pour la déchiffrer.</i></string>
<string name="image_file"><i>Image reçue. Appuyez pour visualiser.</i></string>
<string name="pref_general">Général</string>
<string name="pref_xmpp_resource">Ressource XMPP</string>
<string name="pref_xmpp_resource_summary">Nom permettant d\'identifier ce client XMPP</string>
<string name="pref_accept_files">Accepter les fichiers</string>
@ -107,6 +108,7 @@
<string name="pref_never_send_crash_summary">En envoyant des logs vous aidez au développement de Conversations.</string>
<string name="pref_confirm_messages">Confirmation de lecture</string>
<string name="pref_confirm_messages_summary">Informer l\'expéditeur d\'un message de sa bonne réception.</string>
<string name="pref_ui_options">Options d\'affichage</string>
<string name="openpgp_error">Une erreur s\'est produite via OpenKeychain</string>
<string name="error_decrypting_file">Erreur d\'E/S lors du déchiffrement du fichier</string>
<string name="accept">Accepter</string>
@ -145,6 +147,8 @@
<string name="mgmt_account_edit">Modifier le compte</string>
<string name="mgmt_account_delete">Supprimer</string>
<string name="mgmt_account_disable">Désactiver temporairement</string>
<string name="mgmt_account_publish_avatar">Publier un avatar</string>
<string name="mgmt_account_publish_pgp">Publier la clef publique OpenPGP</string>
<string name="mgmt_account_enable">Activer</string>
<string name="mgmt_account_are_you_sure">Êtes-vous sûr?</string>
<string name="mgmt_account_delete_confirm_text">En supprimant votre compte, votre historique de conversations sera perdu!</string>
@ -169,6 +173,9 @@
<string name="muc_details_other_members">Autres membres</string>
<string name="server_info_carbon_messages">Copies carbone</string>
<string name="server_info_stream_management">Gestion des flux</string>
<string name="server_info_pep">XEP-0163: PEP (Avatars)</string>
<string name="server_info_available">disponible</string>
<string name="server_info_unavailable">indisponible</string>
<string name="missing_public_keys">Aucune annonce de clef publique</string>
<string name="last_seen_now">en ligne à l\'instant</string>
<string name="last_seen_min">en ligne il y a 1 minute</string>
@ -207,6 +214,60 @@
<string name="contact_added_you">Votre correspondant vous a ajouté dans sa liste de contacts</string>
<string name="add_back">Ajouter également</string>
<string name="contact_has_read_up_to_this_point">%s a lu les messages précédents.</string>
<string name="pref_ui_options">Options d\'affichage</string>
<string name="publish">Publier</string>
<string name="touch_to_choose_picture">Toucher l\'avatar pour choisir une image depuis la galerie.</string>
<string name="publish_avatar_explanation">Nota Bene: Les personnes ayant activé les mises jour de présence verront cette image.</string>
<string name="publishing">Mise à jour&#8230;</string>
<string name="error_publish_avatar_server_reject">Le serveur a rejeté votre envoi d\'image</string>
<string name="error_publish_avatar_converting">Une erreur s\'est produite pendant la conversion de votre image.</string>
<string name="error_saving_avatar">Impossible de stocker l\'image sur le disque</string>
<string name="or_long_press_for_default">(Un appui long réinitialise le paramètre par defaut)</string>
<string name="error_publish_avatar_no_server_support">Votre serveur n\'autorise pas l\'envoi d\'avatars</string>
<string name="private_message">chuchoté</string>
<string name="private_message_to">pour %s</string>
<string name="send_private_message_to">Envoyer un message privé à %s</string>
<string name="connect">Se connecter</string>
<string name="account_already_exists">Ce compte existe déjà</string>
<string name="next">suivant</string>
<string name="server_info_session_established">Session établie</string>
<string name="additional_information">Informations supplémentaires</string>
<string name="skip">Passer</string>
<string name="disable_notifications">Désactiver les notifications</string>
<string name="disable_notifications_for_this_conversation">Désactiver les notifications pour cette conversation</string>
<string name="notifications_disabled">Notifications are Désactivées</string>
<string name="enable">Activer</string>
<string name="conference_requires_password">La conférence necessite un mot de passe</string>
<string name="enter_password">Entrer le mot de passe</string>
<string name="missing_presence_updates">Mise à jour de présence non connue</string>
<string name="request_presence_updates">Merci de demander à votre contact de fournir les mises à jour de présence.\n\n<small>Cela permettra de savoir quel matériel utilise votre contact.</small></string>
<string name="request_now">Demander maintenant</string>
<string name="delete_fingerprint">Supprimer l\'empreinte</string>
<string name="sure_delete_fingerprint">Etes-vous sûr de vouloir supprimer l\'empreinte?</string>
<string name="ignore">Ignorer</string>
<string name="without_mutual_presence_updates"><b>Attention:</b> Ceci peut poser problème si l\'un des deux correspondants n\'a pas activé les mises à jour de présence.\n\n<small>Go to contact details to verify your presence subscriptions.</small></string>
<string name="pref_encryption_settings">Paramètres de chiffrement</string>
<string name="pref_force_encryption">Forcer le chiffrement de bout en bout</string>
<string name="pref_force_encryption_summary">Toujours envoyer des messages chiffrés (sauf pour les conférences)</string>
<string name="pref_dont_save_encrypted">Ne pas sauvegarder les messages chiffrés</string>
<string name="pref_dont_save_encrypted_summary">Attention: Celà peut mener à une perte de messages</string>
<string name="pref_expert_options">Options avancées</string>
<string name="pref_expert_options_summary">A utiliser avec précautions</string>
<string name="pref_use_larger_font">Augmenter la taille du texte</string>
<string name="pref_use_larger_font_summary">Augmenter la taille du texte partout dans l\'application</string>
<string name="pref_use_send_button_to_indicate_status">Le bouton Envoyer permet d\'indiquer le statut</string>
<string name="pref_use_indicate_received">Accusé de reception</string>
<string name="pref_use_indicate_received_summary">Les messages recus seront marqués d\'une coche verte si disponible</string>
<string name="pref_use_send_button_to_indicate_status_summary">Adapter la couleur du bouton Envoyer pour indiquer le statut</string>
<string name="pref_expert_options_other">Autres</string>
<string name="pref_conference_name">Nom de la conférence </string>
<string name="pref_conference_name_summary">Identifier la conférence par son nom plutot que par son JID</string>
<string name="toast_message_otr_fingerprint">Empreinte OTR copiée dans le presse-papier!</string>
<string name="conference_banned">Vous êtes interdit de cette conférence</string>
<string name="conference_members_only">Cette conférence est réservée aux membres</string>
<string name="conference_kicked">Vous avez été éjecté de cette conférence</string>
<string name="using_account">utiliser le compte %s</string>
<string name="checking_image">Vérification de l\'image</string>
<string name="image_file_deleted">L\'image a été suprimée</string>
<string name="not_connected_try_again">Vous n\'êtes pas connecté. Merci de retenter plus tard.</string>
</resources>

View file

@ -1,19 +0,0 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="sectionHeader" parent="android:Widget.Holo.Light.TextView">
<item name="android:drawableBottom">@drawable/section_header</item>
<item name="android:drawablePadding">4dp</item>
<item name="android:layout_marginTop">8dp</item>
<item name="android:textSize">14sp</item>
<item name="android:textAllCaps">true</item>
<item name="android:textColor">#5b5b5b</item>
<item name="android:textStyle">bold</item>
</style>
<style name="Divider">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1.5dp</item>
<item name="android:background">#b7b7b7</item>
</style>
</resources>

View file

@ -22,7 +22,7 @@
</string-array>
<string-array name="mute_options_descriptions">
<item>30 minutes</item>
<item>one hour</item>
<item>1 hour</item>
<item>2 hours</item>
<item>8 hours</item>
<item>until further notice</item>

View file

@ -250,13 +250,15 @@
<string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string>
<string name="pref_dont_save_encrypted">Dont save encrypted messages</string>
<string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string>
<string name="pref_enable_legacy_ssl">Enable legacy SSL</string>
<string name="pref_enable_legacy_ssl_summary">Enables SSLv3 support for legacy servers. Warning: SSLv3 is considered insecure.</string>
<string name="pref_expert_options">Expert options</string>
<string name="pref_expert_options_summary">Please be very careful with those</string>
<string name="pref_expert_options_summary">Please be careful with these</string>
<string name="pref_use_larger_font">Increase font size</string>
<string name="pref_use_larger_font_summary">Use larger font sizes across the entire app</string>
<string name="pref_use_send_button_to_indicate_status">Send button indicates status</string>
<string name="pref_use_indicate_received">Request message receipts</string>
<string name="pref_use_indicate_received_summary">Received masseges will be marked with a green tick. Be aware that this might no work in every case.</string>
<string name="pref_use_indicate_received_summary">Received messages will be marked with a green tick if supported</string>
<string name="pref_use_send_button_to_indicate_status_summary">Colorize send button to indicate contact status</string>
<string name="pref_expert_options_other">Other</string>
<string name="pref_conference_name">Conference name</string>
@ -265,5 +267,20 @@
<string name="conference_banned">You are banned from this conference</string>
<string name="conference_members_only">This conference is members only</string>
<string name="conference_kicked">You have been kicked from this conference</string>
<string name="using_account">using account %s</string>
<string name="checking_image">Checking image on HTTP host</string>
<string name="image_file_deleted">The image file has been deleted</string>
<string name="not_connected_try_again">You are not connected. Try again later</string>
<string name="check_image_filesize">Check image file size</string>
<string name="message_options">Message options</string>
<string name="copy_text">Copy text</string>
<string name="share_image">Share image</string>
<string name="copy_original_url">Copy original URL</string>
<string name="send_again">Send again</string>
<string name="image_url">Image URL</string>
<string name="message_text">Message text</string>
<string name="url_copied_to_clipboard">URL copied to clipboard</string>
<string name="message_copied_to_clipboard">Message copied to clipboard</string>
<string name="image_transmission_failed">Image transmission failed</string>
</resources>

View file

@ -1,15 +1,4 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="sectionHeader" parent="android:Widget.Holo.Light.TextView">
<item name="android:drawableBottom">@drawable/section_header</item>
<item name="android:drawablePadding">4dp</item>
<item name="android:layout_marginTop">8dp</item>
<item name="android:textSize">14sp</item>
<item name="android:textAllCaps">true</item>
<item name="android:textColor">@color/primarytext</item>
<item name="android:textStyle">bold</item>
</style>
<style name="Divider">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1.5dp</item>

View file

@ -51,6 +51,7 @@
android:title="@string/pref_sound" />
<CheckBoxPreference
android:defaultValue="true"
android:dependency="show_notification"
android:key="always_notify_in_conference"
android:summary="@string/pref_conference_notifications_summary"
@ -88,6 +89,11 @@
android:key="dont_save_encrypted"
android:summary="@string/pref_dont_save_encrypted_summary"
android:title="@string/pref_dont_save_encrypted" />
<CheckBoxPreference
android:defaultValue="false"
android:key="enable_legacy_ssl"
android:summary="@string/pref_enable_legacy_ssl_summary"
android:title="@string/pref_enable_legacy_ssl" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/pref_expert_options_other" >
<CheckBoxPreference

View file

@ -10,7 +10,8 @@ public final class Config {
public static final int PING_MIN_INTERVAL = 30;
public static final int PING_TIMEOUT = 10;
public static final int CONNECT_TIMEOUT = 90;
public static final int CARBON_GRACE_PERIOD = 120;
public static final int CARBON_GRACE_PERIOD = 60;
public static final int MINI_GRACE_PERIOD = 750;
public static final int AVATAR_SIZE = 192;
public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP;

View file

@ -8,25 +8,24 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpSignatureResult;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.UiCallback;
import eu.siacs.conversations.xmpp.jingle.JingleFile;
import android.app.PendingIntent;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.util.Log;
import android.net.Uri;
public class PgpEngine {
private OpenPgpApi api;
@ -39,7 +38,6 @@ public class PgpEngine {
public void decrypt(final Message message,
final UiCallback<Message> callback) {
Log.d(Config.LOGTAG, "decrypting message " + message.getUuid());
Intent params = new Intent();
params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message
@ -60,6 +58,10 @@ public class PgpEngine {
if (message.getEncryption() == Message.ENCRYPTION_PGP) {
message.setBody(os.toString());
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
if (message.trusted() && message.bodyContainsDownloadable()) {
mXmppConnectionService.getHttpConnectionManager()
.createNewConnection(message);
}
callback.success(message);
}
} catch (IOException e) {
@ -74,9 +76,6 @@ public class PgpEngine {
message);
return;
case OpenPgpApi.RESULT_CODE_ERROR:
OpenPgpError error = result
.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
Log.d(Config.LOGTAG, error.getMessage());
callback.error(R.string.openpgp_error, message);
return;
default:
@ -86,10 +85,10 @@ public class PgpEngine {
});
} else if (message.getType() == Message.TYPE_IMAGE) {
try {
final JingleFile inputFile = this.mXmppConnectionService
.getFileBackend().getJingleFile(message, false);
final JingleFile outputFile = this.mXmppConnectionService
.getFileBackend().getJingleFile(message, true);
final DownloadableFile inputFile = this.mXmppConnectionService
.getFileBackend().getFile(message, false);
final DownloadableFile outputFile = this.mXmppConnectionService
.getFileBackend().getFile(message, true);
outputFile.createNewFile();
InputStream is = new FileInputStream(inputFile);
OutputStream os = new FileOutputStream(outputFile);
@ -100,19 +99,32 @@ public class PgpEngine {
switch (result.getIntExtra(OpenPgpApi.RESULT_CODE,
OpenPgpApi.RESULT_CODE_ERROR)) {
case OpenPgpApi.RESULT_CODE_SUCCESS:
URL url = message.getImageParams().url;
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(
outputFile.getAbsolutePath(), options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
message.setBody(Long.toString(outputFile.getSize())
+ ',' + imageWidth + ',' + imageHeight);
if (url == null) {
message.setBody(Long.toString(outputFile
.getSize())
+ '|'
+ imageWidth
+ '|'
+ imageHeight);
} else {
message.setBody(url.toString() + "|"
+ Long.toString(outputFile.getSize())
+ '|' + imageWidth + '|' + imageHeight);
}
message.setEncryption(Message.ENCRYPTION_DECRYPTED);
PgpEngine.this.mXmppConnectionService
.updateMessage(message);
PgpEngine.this.mXmppConnectionService
.updateConversationUi();
inputFile.delete();
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(outputFile));
mXmppConnectionService.sendBroadcast(intent);
callback.success(message);
return;
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
@ -197,10 +209,10 @@ public class PgpEngine {
});
} else if (message.getType() == Message.TYPE_IMAGE) {
try {
JingleFile inputFile = this.mXmppConnectionService
.getFileBackend().getJingleFile(message, true);
JingleFile outputFile = this.mXmppConnectionService
.getFileBackend().getJingleFile(message, false);
DownloadableFile inputFile = this.mXmppConnectionService
.getFileBackend().getFile(message, true);
DownloadableFile outputFile = this.mXmppConnectionService
.getFileBackend().getFile(message, false);
outputFile.createNewFile();
InputStream is = new FileInputStream(inputFile);
OutputStream os = new FileOutputStream(outputFile);
@ -226,9 +238,11 @@ public class PgpEngine {
}
});
} catch (FileNotFoundException e) {
Log.d(Config.LOGTAG, "file not found: " + e.getMessage());
callback.error(R.string.openpgp_error, message);
return;
} catch (IOException e) {
Log.d(Config.LOGTAG, "io exception during file encrypt");
callback.error(R.string.openpgp_error, message);
return;
}
}
}
@ -272,11 +286,6 @@ public class PgpEngine {
case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
return 0;
case OpenPgpApi.RESULT_CODE_ERROR:
Log.d(Config.LOGTAG,
"openpgp error: "
+ ((OpenPgpError) result
.getParcelableExtra(OpenPgpApi.RESULT_ERROR))
.getMessage());
return 0;
}
return 0;

View file

@ -11,16 +11,14 @@ import net.java.otr4j.crypto.OtrCryptoException;
import org.json.JSONException;
import org.json.JSONObject;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrEngine;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.XmppConnection;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.SystemClock;
public class Account extends AbstractEntity {
@ -64,16 +62,14 @@ public class Account extends AbstractEntity {
protected boolean online = false;
transient OtrEngine otrEngine = null;
transient XmppConnection xmppConnection = null;
transient protected Presences presences = new Presences();
private OtrEngine otrEngine = null;
private XmppConnection xmppConnection = null;
private Presences presences = new Presences();
private long mEndGracePeriod = 0L;
private String otrFingerprint;
private Roster roster = null;
private List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<Conversation>();
public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<Conversation>();
@ -160,8 +156,12 @@ public class Account extends AbstractEntity {
}
public boolean hasErrorStatus() {
return getStatus() > STATUS_NO_INTERNET
&& (getXmppConnection().getAttempt() >= 2);
if (getXmppConnection() == null) {
return false;
} else {
return getStatus() > STATUS_NO_INTERNET
&& (getXmppConnection().getAttempt() >= 2);
}
}
public void setResource(String resource) {
@ -341,20 +341,6 @@ public class Account extends AbstractEntity {
return false;
}
public Bitmap getImage(Context context, int size) {
if (this.avatar != null) {
Bitmap bm = FileBackend.getAvatar(this.avatar, size, context);
if (bm == null) {
return UIHelper.getContactPicture(getJid(), size, context,
false);
} else {
return bm;
}
} else {
return UIHelper.getContactPicture(getJid(), size, context, false);
}
}
public boolean setAvatar(String filename) {
if (this.avatar != null && this.avatar.equals(filename)) {
return false;
@ -397,4 +383,17 @@ public class Account extends AbstractEntity {
return R.string.account_status_unknown;
}
}
public void activateGracePeriod() {
this.mEndGracePeriod = SystemClock.elapsedRealtime()
+ (Config.CARBON_GRACE_PERIOD * 1000);
}
public void deactivateGracePeriod() {
this.mEndGracePeriod = 0L;
}
public boolean inGracePeriod() {
return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
}
}

View file

@ -2,9 +2,6 @@ package eu.siacs.conversations.entities;
import java.util.Locale;
import android.content.Context;
import android.graphics.Bitmap;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
public class Bookmark extends Element implements ListItem {
@ -120,21 +117,14 @@ public class Bookmark extends Element implements ListItem {
return this.account;
}
@Override
public Bitmap getImage(int dpSize, Context context) {
if (this.mJoinedConversation == null) {
return UIHelper.getContactPicture(getDisplayName(), dpSize,
context, false);
} else {
return UIHelper.getContactPicture(this.mJoinedConversation, dpSize,
context, false);
}
}
public void setConversation(Conversation conversation) {
this.mJoinedConversation = conversation;
}
public Conversation getConversation() {
return this.mJoinedConversation;
}
public String getName() {
return this.getAttribute("name");
}

View file

@ -8,13 +8,9 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
public class Contact implements ListItem {
public static final String TABLENAME = "contacts";
@ -330,20 +326,6 @@ public class Contact implements ListItem {
}
}
@Override
public Bitmap getImage(int size, Context context) {
if (this.avatar != null) {
Bitmap bm = FileBackend.getAvatar(avatar, size, context);
if (bm == null) {
return UIHelper.getContactPicture(this, size, context, false);
} else {
return bm;
}
} else {
return UIHelper.getContactPicture(this, size, context, false);
}
}
public boolean setAvatar(String filename) {
if (this.avatar != null && this.avatar.equals(filename)) {
return false;
@ -353,6 +335,10 @@ public class Contact implements ListItem {
}
}
public String getAvatar() {
return this.avatar;
}
public boolean deleteOtrFingerprint(String fingerprint) {
boolean success = false;
try {
@ -374,4 +360,8 @@ public class Contact implements ListItem {
return false;
}
}
public boolean trusted() {
return getOption(Options.FROM) && getOption(Options.TO);
}
}

View file

@ -1,14 +1,13 @@
package eu.siacs.conversations.entities;
import java.security.interfaces.DSAPublicKey;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import org.json.JSONException;
import org.json.JSONObject;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.UIHelper;
import net.java.otr4j.OtrException;
import net.java.otr4j.crypto.OtrCryptoEngineImpl;
@ -17,9 +16,7 @@ import net.java.otr4j.session.SessionID;
import net.java.otr4j.session.SessionImpl;
import net.java.otr4j.session.SessionStatus;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.os.SystemClock;
public class Conversation extends AbstractEntity {
@ -57,8 +54,8 @@ public class Conversation extends AbstractEntity {
private String nextPresence;
private transient CopyOnWriteArrayList<Message> messages = null;
private transient Account account = null;
protected ArrayList<Message> messages = new ArrayList<Message>();
protected Account account = null;
private transient SessionImpl otrSession;
@ -68,12 +65,10 @@ public class Conversation extends AbstractEntity {
private transient MucOptions mucOptions = null;
//private transient String latestMarkableMessageId;
// private transient String latestMarkableMessageId;
private byte[] symmetricKey;
private boolean otrSessionNeedsStarting = false;
private Bookmark bookmark;
public Conversation(String name, Account account, String contactJid,
@ -106,17 +101,6 @@ public class Conversation extends AbstractEntity {
}
public List<Message> getMessages() {
if (messages == null) {
this.messages = new CopyOnWriteArrayList<Message>(); // prevent null
// pointer
}
// populate with Conversation (this)
for (Message msg : messages) {
msg.setConversation(this);
}
return messages;
}
@ -142,8 +126,9 @@ public class Conversation extends AbstractEntity {
if (this.messages == null) {
return null;
}
for(int i = this.messages.size() - 1; i >= 0; --i) {
if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED && this.messages.get(i).markable) {
for (int i = this.messages.size() - 1; i >= 0; --i) {
if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
&& this.messages.get(i).markable) {
if (this.messages.get(i).isRead()) {
return null;
} else {
@ -166,7 +151,7 @@ public class Conversation extends AbstractEntity {
}
}
public void setMessages(CopyOnWriteArrayList<Message> msgs) {
public void setMessages(ArrayList<Message> msgs) {
this.messages = msgs;
}
@ -263,10 +248,7 @@ public class Conversation extends AbstractEntity {
try {
if (sendStart) {
this.otrSession.startSession();
this.otrSessionNeedsStarting = false;
return this.otrSession;
} else {
this.otrSessionNeedsStarting = true;
}
return this.otrSession;
} catch (OtrException e) {
@ -282,12 +264,12 @@ public class Conversation extends AbstractEntity {
public void resetOtrSession() {
this.otrFingerprint = null;
this.otrSessionNeedsStarting = false;
this.otrSession = null;
}
public void startOtrIfNeeded() {
if (this.otrSession != null && this.otrSessionNeedsStarting) {
if (this.otrSession != null
&& this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
try {
this.otrSession.startSession();
} catch (OtrException e) {
@ -296,18 +278,23 @@ public class Conversation extends AbstractEntity {
}
}
public void endOtrIfNeeded() {
public boolean endOtrIfNeeded() {
if (this.otrSession != null) {
if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
try {
this.otrSession.endSession();
this.resetOtrSession();
return true;
} catch (OtrException e) {
this.resetOtrSession();
return false;
}
} else {
this.resetOtrSession();
return false;
}
} else {
return false;
}
}
@ -339,9 +326,8 @@ public class Conversation extends AbstractEntity {
public synchronized MucOptions getMucOptions() {
if (this.mucOptions == null) {
this.mucOptions = new MucOptions(this.getAccount());
this.mucOptions = new MucOptions(this);
}
this.mucOptions.setConversation(this);
return this.mucOptions;
}
@ -438,14 +424,6 @@ public class Conversation extends AbstractEntity {
return this.bookmark;
}
public Bitmap getImage(Context context, int size) {
if (mode == MODE_SINGLE) {
return getContact().getImage(size, context);
} else {
return UIHelper.getContactPicture(this, size, context, false);
}
}
public boolean hasDuplicateMessage(Message message) {
for (int i = this.getMessages().size() - 1; i >= 0; --i) {
if (this.messages.get(i).equals(message)) {
@ -506,4 +484,17 @@ public class Conversation extends AbstractEntity {
}
}
}
public void add(Message message) {
message.setConversation(this);
synchronized (this.messages) {
this.messages.add(message);
}
}
public void addAll(int index, List<Message> messages) {
synchronized (this.messages) {
this.messages.addAll(index, messages);
}
}
}

View file

@ -1,5 +1,21 @@
package eu.siacs.conversations.entities;
public interface Downloadable {
public void start();
public final String[] VALID_EXTENSIONS = { "webp", "jpeg", "jpg", "png" };
public final String[] VALID_CRYPTO_EXTENSIONS = { "pgp", "gpg", "otr" };
public static final int STATUS_UNKNOWN = 0x200;
public static final int STATUS_CHECKING = 0x201;
public static final int STATUS_FAILED = 0x202;
public static final int STATUS_OFFER = 0x203;
public static final int STATUS_DOWNLOADING = 0x204;
public static final int STATUS_DELETED = 0x205;
public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206;
public boolean start();
public int getStatus();
public long getFileSize();
}

View file

@ -0,0 +1,154 @@
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.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 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 byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
public DownloadableFile(String path) {
super(path);
}
public long getSize() {
return super.length();
}
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 void setExpectedSize(long size) {
this.expectedSize = size;
}
public String getSha1Sum() {
return this.sha1sum;
}
public void setSha1Sum(String sum) {
this.sha1sum = sum;
}
public void setKey(byte[] key) {
if (key.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");
this.iv = iv;
} else if (key.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) {
byte[] secretKey = new byte[16];
System.arraycopy(key, 0, secretKey, 0, 16);
this.aeskey = new SecretKeySpec(secretKey, "AES");
}
}
public Key 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;
}
}
}
}

View file

@ -1,12 +1,7 @@
package eu.siacs.conversations.entities;
import android.content.Context;
import android.graphics.Bitmap;
public interface ListItem extends Comparable<ListItem> {
public String getDisplayName();
public String getJid();
public Bitmap getImage(int dpSize, Context context);
}

View file

@ -1,23 +1,21 @@
package eu.siacs.conversations.entities;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Arrays;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
public class Message extends AbstractEntity {
public static final String TABLENAME = "messages";
public static final int STATUS_RECEPTION_FAILED = -3;
public static final int STATUS_RECEIVED_OFFER = -2;
public static final int STATUS_RECEIVING = -1;
public static final int STATUS_RECEIVED = 0;
public static final int STATUS_UNSEND = 1;
public static final int STATUS_SEND = 2;
public static final int STATUS_SEND_FAILED = 3;
public static final int STATUS_SEND_REJECTED = 4;
public static final int STATUS_WAITING = 5;
public static final int STATUS_OFFERED = 6;
public static final int STATUS_SEND_RECEIVED = 7;
@ -61,6 +59,9 @@ public class Message extends AbstractEntity {
protected Downloadable downloadable = null;
public boolean markable = false;
private Message mNextMessage = null;
private Message mPreviousMessage = null;
private Message() {
}
@ -131,14 +132,8 @@ public class Message extends AbstractEntity {
if (this.trueCounterpart == null) {
return null;
} else {
Account account = this.conversation.getAccount();
Contact contact = account.getRoster().getContact(
this.trueCounterpart);
if (contact.showInRoster()) {
return contact;
} else {
return null;
}
return this.conversation.getAccount().getRoster()
.getContactFromRoster(this.trueCounterpart);
}
}
}
@ -147,22 +142,6 @@ public class Message extends AbstractEntity {
return body;
}
public String getReadableBody(Context context) {
if ((encryption == ENCRYPTION_PGP) && (type == TYPE_TEXT)) {
return context.getText(R.string.encrypted_message_received)
.toString();
} else if ((encryption == ENCRYPTION_OTR) && (type == TYPE_IMAGE)) {
return context.getText(R.string.encrypted_image_received)
.toString();
} else if (encryption == ENCRYPTION_DECRYPTION_FAILED) {
return context.getText(R.string.decryption_failed).toString();
} else if (type == TYPE_IMAGE) {
return context.getText(R.string.image_file).toString();
} else {
return body.trim();
}
}
public long getTimeSent() {
return timeSent;
}
@ -301,21 +280,34 @@ public class Message extends AbstractEntity {
}
public Message next() {
int index = this.conversation.getMessages().indexOf(this);
if (index < 0 || index >= this.conversation.getMessages().size() - 1) {
return null;
} else {
return this.conversation.getMessages().get(index + 1);
if (this.mNextMessage == null) {
synchronized (this.conversation.messages) {
int index = this.conversation.messages.indexOf(this);
if (index < 0
|| index >= this.conversation.getMessages().size() - 1) {
this.mNextMessage = null;
} else {
this.mNextMessage = this.conversation.messages
.get(index + 1);
}
}
}
return this.mNextMessage;
}
public Message prev() {
int index = this.conversation.getMessages().indexOf(this);
if (index <= 0 || index > this.conversation.getMessages().size()) {
return null;
} else {
return this.conversation.getMessages().get(index - 1);
if (this.mPreviousMessage == null) {
synchronized (this.conversation.messages) {
int index = this.conversation.messages.indexOf(this);
if (index <= 0 || index > this.conversation.messages.size()) {
this.mPreviousMessage = null;
} else {
this.mPreviousMessage = this.conversation.messages
.get(index - 1);
}
}
}
return this.mPreviousMessage;
}
public boolean mergable(Message message) {
@ -323,6 +315,8 @@ public class Message extends AbstractEntity {
return false;
}
return (message.getType() == Message.TYPE_TEXT
&& this.getDownloadable() == null
&& message.getDownloadable() == null
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& this.getType() == message.getType()
&& this.getEncryption() == message.getEncryption()
@ -332,7 +326,9 @@ public class Message extends AbstractEntity {
.getStatus() == Message.STATUS_SEND_RECEIVED) && (message
.getStatus() == Message.STATUS_UNSEND
|| message.getStatus() == Message.STATUS_SEND || message
.getStatus() == Message.STATUS_SEND_DISPLAYED)))));
.getStatus() == Message.STATUS_SEND_DISPLAYED))))
&& !message.bodyContainsDownloadable()
&& !this.bodyContainsDownloadable());
}
public String getMergedBody() {
@ -369,4 +365,148 @@ public class Message extends AbstractEntity {
return prev.mergable(this);
}
}
public boolean trusted() {
Contact contact = this.getContact();
return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
}
public boolean bodyContainsDownloadable() {
try {
URL url = new URL(this.getBody());
if (!url.getProtocol().equalsIgnoreCase("http")
&& !url.getProtocol().equalsIgnoreCase("https")) {
return false;
}
if (url.getPath() == null) {
return false;
}
String[] pathParts = url.getPath().split("/");
String filename;
if (pathParts.length > 0) {
filename = pathParts[pathParts.length - 1];
} else {
filename = pathParts[0];
}
String[] extensionParts = filename.split("\\.");
if (extensionParts.length == 2
&& Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
extensionParts[extensionParts.length - 1])) {
return true;
} else if (extensionParts.length == 3
&& Arrays
.asList(Downloadable.VALID_CRYPTO_EXTENSIONS)
.contains(extensionParts[extensionParts.length - 1])
&& Arrays.asList(Downloadable.VALID_EXTENSIONS).contains(
extensionParts[extensionParts.length - 2])) {
return true;
} else {
return false;
}
} catch (MalformedURLException e) {
return false;
}
}
public ImageParams getImageParams() {
ImageParams params = getLegacyImageParams();
if (params!=null) {
return params;
}
params = new ImageParams();
if (this.downloadable != null) {
params.size = this.downloadable.getFileSize();
}
if (body == null) {
return params;
}
String parts[] = body.split("\\|");
if (parts.length == 1) {
try {
params.size = Long.parseLong(parts[0]);
} catch (NumberFormatException e) {
params.origin = parts[0];
try {
params.url = new URL(parts[0]);
} catch (MalformedURLException e1) {
params.url = null;
}
}
} else if (parts.length == 3) {
try {
params.size = Long.parseLong(parts[0]);
} catch (NumberFormatException e) {
params.size = 0;
}
try {
params.width = Integer.parseInt(parts[1]);
} catch (NumberFormatException e) {
params.width = 0;
}
try {
params.height = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
params.height = 0;
}
} else if (parts.length == 4) {
params.origin = parts[0];
try {
params.url = new URL(parts[0]);
} catch (MalformedURLException e1) {
params.url = null;
}
try {
params.size = Long.parseLong(parts[1]);
} catch (NumberFormatException e) {
params.size = 0;
}
try {
params.width = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
params.width = 0;
}
try {
params.height = Integer.parseInt(parts[3]);
} catch (NumberFormatException e) {
params.height = 0;
}
}
return params;
}
public ImageParams getLegacyImageParams() {
ImageParams params = new ImageParams();
if (body == null) {
return params;
}
String parts[] = body.split(",");
if (parts.length == 3) {
try {
params.size = Long.parseLong(parts[0]);
} catch (NumberFormatException e) {
return null;
}
try {
params.width = Integer.parseInt(parts[1]);
} catch (NumberFormatException e) {
return null;
}
try {
params.height = Integer.parseInt(parts[2]);
} catch (NumberFormatException e) {
return null;
}
return params;
} else {
return null;
}
}
public class ImageParams {
public URL url;
public long size = 0;
public int width = 0;
public int height = 0;
public String origin;
}
}

View file

@ -102,6 +102,10 @@ public class MucOptions {
public long getPgpKeyId() {
return this.pgpKeyId;
}
public Contact getContact() {
return account.getRoster().getContactFromRoster(getJid());
}
}
private Account account;
@ -116,8 +120,9 @@ public class MucOptions {
private String joinnick;
private String password = null;
public MucOptions(Account account) {
this.account = account;
public MucOptions(Conversation conversation) {
this.account = conversation.getAccount();
this.conversation = conversation;
}
public void deleteUser(String name) {
@ -253,10 +258,6 @@ public class MucOptions {
this.joinnick = nick;
}
public void setConversation(Conversation conversation) {
this.conversation = conversation;
}
public boolean online() {
return this.isOnline;
}
@ -361,4 +362,8 @@ public class MucOptions {
conversation
.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
}
public Conversation getConversation() {
return this.conversation;
}
}

View file

@ -14,7 +14,10 @@ public class Roster {
this.account = account;
}
public Contact getContactAsShownInRoster(String jid) {
public Contact getContactFromRoster(String jid) {
if (jid == null) {
return null;
}
String cleanJid = jid.split("/", 2)[0];
Contact contact = contacts.get(cleanJid);
if (contact != null && contact.showInRoster()) {

View file

@ -0,0 +1,270 @@
package eu.siacs.conversations.http;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.X509TrustManager;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.net.Uri;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
public class HttpConnection implements Downloadable {
private HttpConnectionManager mHttpConnectionManager;
private XmppConnectionService mXmppConnectionService;
private URL mUrl;
private Message message;
private DownloadableFile file;
private int mStatus = Downloadable.STATUS_UNKNOWN;
private boolean acceptedAutomatically = false;
public HttpConnection(HttpConnectionManager manager) {
this.mHttpConnectionManager = manager;
this.mXmppConnectionService = manager.getXmppConnectionService();
}
@Override
public boolean start() {
if (mXmppConnectionService.hasInternetConnection()) {
if (this.mStatus == STATUS_OFFER_CHECK_FILESIZE) {
checkFileSize(true);
} else {
new Thread(new FileDownloader(true)).start();
}
return true;
} else {
return false;
}
}
public void init(Message message) {
this.message = message;
this.message.setDownloadable(this);
try {
mUrl = new URL(message.getBody());
String path = mUrl.getPath();
if (path != null && (path.endsWith(".pgp") || path.endsWith(".gpg"))) {
this.message.setEncryption(Message.ENCRYPTION_PGP);
} else if (message.getEncryption() != Message.ENCRYPTION_OTR) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
this.file = mXmppConnectionService.getFileBackend().getFile(
message, false);
String reference = mUrl.getRef();
if (reference != null && reference.length() == 96) {
this.file.setKey(CryptoHelper.hexToBytes(reference));
}
if (this.message.getEncryption() == Message.ENCRYPTION_OTR
&& this.file.getKey() == null) {
this.message.setEncryption(Message.ENCRYPTION_NONE);
}
checkFileSize(false);
} catch (MalformedURLException e) {
this.cancel();
}
}
private void checkFileSize(boolean interactive) {
new Thread(new FileSizeChecker(interactive)).start();
}
public void cancel() {
mHttpConnectionManager.finishConnection(this);
message.setDownloadable(null);
mXmppConnectionService.updateConversationUi();
}
private void finish() {
Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
intent.setData(Uri.fromFile(file));
mXmppConnectionService.sendBroadcast(intent);
message.setDownloadable(null);
mHttpConnectionManager.finishConnection(this);
mXmppConnectionService.updateConversationUi();
if (acceptedAutomatically) {
mXmppConnectionService.getNotificationService().push(message);
}
}
private void changeStatus(int status) {
this.mStatus = status;
mXmppConnectionService.updateConversationUi();
}
private void setupTrustManager(HttpsURLConnection connection,
boolean interactive) {
X509TrustManager trustManager;
HostnameVerifier hostnameVerifier;
if (interactive) {
trustManager = mXmppConnectionService.getMemorizingTrustManager();
hostnameVerifier = mXmppConnectionService
.getMemorizingTrustManager().wrapHostnameVerifier(
new StrictHostnameVerifier());
} else {
trustManager = mXmppConnectionService.getMemorizingTrustManager()
.getNonInteractive();
hostnameVerifier = mXmppConnectionService
.getMemorizingTrustManager()
.wrapHostnameVerifierNonInteractive(
new StrictHostnameVerifier());
}
try {
SSLContext sc = SSLContext.getInstance("TLS");
sc.init(null, new X509TrustManager[] { trustManager },
mXmppConnectionService.getRNG());
connection.setSSLSocketFactory(sc.getSocketFactory());
connection.setHostnameVerifier(hostnameVerifier);
} catch (KeyManagementException e) {
return;
} catch (NoSuchAlgorithmException e) {
return;
}
}
private class FileSizeChecker implements Runnable {
private boolean interactive = false;
public FileSizeChecker(boolean interactive) {
this.interactive = interactive;
}
@Override
public void run() {
long size;
try {
size = retrieveFileSize();
} catch (SSLHandshakeException e) {
changeStatus(STATUS_OFFER_CHECK_FILESIZE);
HttpConnection.this.acceptedAutomatically = false;
HttpConnection.this.mXmppConnectionService.getNotificationService().push(message);
return;
} catch (IOException e) {
cancel();
return;
}
file.setExpectedSize(size);
if (size <= mHttpConnectionManager.getAutoAcceptFileSize()) {
HttpConnection.this.acceptedAutomatically = true;
new Thread(new FileDownloader(interactive)).start();
} else {
changeStatus(STATUS_OFFER);
HttpConnection.this.acceptedAutomatically = false;
HttpConnection.this.mXmppConnectionService.getNotificationService().push(message);
}
}
private long retrieveFileSize() throws IOException,
SSLHandshakeException {
changeStatus(STATUS_CHECKING);
HttpURLConnection connection = (HttpURLConnection) mUrl
.openConnection();
connection.setRequestMethod("HEAD");
if (connection instanceof HttpsURLConnection) {
setupTrustManager((HttpsURLConnection) connection, interactive);
}
connection.connect();
String contentLength = connection.getHeaderField("Content-Length");
if (contentLength == null) {
throw new IOException();
}
try {
return Long.parseLong(contentLength, 10);
} catch (NumberFormatException e) {
throw new IOException();
}
}
}
private class FileDownloader implements Runnable {
private boolean interactive = false;
public FileDownloader(boolean interactive) {
this.interactive = interactive;
}
@Override
public void run() {
try {
changeStatus(STATUS_DOWNLOADING);
download();
updateImageBounds();
finish();
} catch (SSLHandshakeException e) {
changeStatus(STATUS_OFFER);
} catch (IOException e) {
cancel();
}
}
private void download() throws SSLHandshakeException, IOException {
HttpURLConnection connection = (HttpURLConnection) mUrl
.openConnection();
if (connection instanceof HttpsURLConnection) {
setupTrustManager((HttpsURLConnection) connection, interactive);
}
connection.connect();
BufferedInputStream is = new BufferedInputStream(
connection.getInputStream());
OutputStream os = file.createOutputStream();
int count = -1;
byte[] buffer = new byte[1024];
while ((count = is.read(buffer)) != -1) {
os.write(buffer, 0, count);
}
os.flush();
os.close();
is.close();
}
private void updateImageBounds() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
message.setBody(mUrl.toString() + "|" + file.getSize() + '|'
+ imageWidth + '|' + imageHeight);
message.setType(Message.TYPE_IMAGE);
mXmppConnectionService.updateMessage(message);
}
}
@Override
public int getStatus() {
return this.mStatus;
}
@Override
public long getFileSize() {
if (this.file != null) {
return this.file.getExpectedSize();
} else {
return 0;
}
}
}

View file

@ -0,0 +1,28 @@
package eu.siacs.conversations.http;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
public class HttpConnectionManager extends AbstractConnectionManager {
public HttpConnectionManager(XmppConnectionService service) {
super(service);
}
private List<HttpConnection> connections = new CopyOnWriteArrayList<HttpConnection>();
public HttpConnection createNewConnection(Message message) {
HttpConnection connection = new HttpConnection(this);
connection.init(message);
this.connections.add(connection);
return connection;
}
public void finishConnection(HttpConnection connection) {
this.connections.remove(connection);
}
}

View file

@ -256,7 +256,6 @@ public class MessageParser extends AbstractParser implements
return null;
}
}
return finishedMessage;
}
@ -348,10 +347,18 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.databaseBackend
.updateAccount(account);
}
mXmppConnectionService.getAvatarService().clear(
account);
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateAccountUi();
} else {
Contact contact = account.getRoster().getContact(
from);
contact.setAvatar(avatar.getFilename());
mXmppConnectionService.getAvatarService().clear(
contact);
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateRosterUi();
}
} else {
mXmppConnectionService.fetchAvatar(account, avatar);
@ -417,8 +424,7 @@ public class MessageParser extends AbstractParser implements
message = this.parseCarbonMessage(packet, account);
if (message != null) {
if (message.getStatus() == Message.STATUS_SEND) {
mXmppConnectionService.getNotificationService()
.activateGracePeriod();
account.activateGracePeriod();
notify = false;
mXmppConnectionService.markRead(
message.getConversation(), false);
@ -440,8 +446,7 @@ public class MessageParser extends AbstractParser implements
} else {
mXmppConnectionService.markRead(message.getConversation(),
false);
mXmppConnectionService.getNotificationService()
.activateGracePeriod();
account.activateGracePeriod();
notify = false;
}
}
@ -471,13 +476,26 @@ public class MessageParser extends AbstractParser implements
}
}
Conversation conversation = message.getConversation();
conversation.getMessages().add(message);
conversation.add(message);
if (message.getStatus() == Message.STATUS_RECEIVED
&& conversation.getOtrSession() != null
&& !conversation.getOtrSession().getSessionID().getUserID()
.equals(message.getPresence())) {
conversation.endOtrIfNeeded();
}
if (packet.getType() != MessagePacket.TYPE_ERROR) {
if (message.getEncryption() == Message.ENCRYPTION_NONE
|| mXmppConnectionService.saveEncryptedMessages()) {
mXmppConnectionService.databaseBackend.createMessage(message);
}
}
if (message.trusted() && message.bodyContainsDownloadable()) {
this.mXmppConnectionService.getHttpConnectionManager()
.createNewConnection(message);
notify = false;
}
notify = notify && !conversation.isMuted();
if (notify) {
mXmppConnectionService.getNotificationService().push(message);

View file

@ -29,6 +29,7 @@ public class PresenceParser extends AbstractParser implements
if (before != muc.getMucOptions().online()) {
mXmppConnectionService.updateConversationUi();
}
mXmppConnectionService.getAvatarService().clear(muc);
}
} else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) {
Conversation muc = mXmppConnectionService.find(account, packet
@ -39,6 +40,7 @@ public class PresenceParser extends AbstractParser implements
if (before != muc.getMucOptions().online()) {
mXmppConnectionService.updateConversationUi();
}
mXmppConnectionService.getAvatarService().clear(muc);
}
}
}
@ -58,8 +60,7 @@ public class PresenceParser extends AbstractParser implements
Presences.parseShow(packet.findChild("show")));
} else if (type.equals("unavailable")) {
account.removePresence(fromParts[1]);
mXmppConnectionService.getNotificationService()
.deactivateGracePeriod();
account.deactivateGracePeriod();
}
}
} else {

View file

@ -11,6 +11,7 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Roster;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
@ -151,14 +152,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return list;
}
public CopyOnWriteArrayList<Message> getMessages(
Conversation conversations, int limit) {
public ArrayList<Message> getMessages(Conversation conversations, int limit) {
return getMessages(conversations, limit, -1);
}
public CopyOnWriteArrayList<Message> getMessages(Conversation conversation,
int limit, long timestamp) {
CopyOnWriteArrayList<Message> list = new CopyOnWriteArrayList<Message>();
public ArrayList<Message> getMessages(Conversation conversation, int limit,
long timestamp) {
ArrayList<Message> list = new ArrayList<Message>();
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor;
if (timestamp == -1) {
@ -177,7 +177,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (cursor.getCount() > 0) {
cursor.moveToLast();
do {
list.add(Message.fromCursor(cursor));
Message message = Message.fromCursor(cursor);
message.setConversation(conversation);
list.add(message);
} while (cursor.moveToPrevious());
}
return list;
@ -231,10 +233,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from "
+ Account.TABLENAME + " where not options & (1 <<1)", null);
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
return (count > 0);
try {
cursor.moveToFirst();
int count = cursor.getInt(0);
cursor.close();
return (count > 0);
} catch (SQLiteCantOpenDatabaseException e) {
return true; // better safe than sorry
}
}
@Override
@ -326,4 +332,22 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.moveToFirst();
return Account.fromCursor(cursor);
}
public List<Message> getImageMessages(Conversation conversation) {
ArrayList<Message> list = new ArrayList<Message>();
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor;
String[] selectionArgs = { conversation.getUuid(), String.valueOf(Message.TYPE_IMAGE) };
cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
+ "=? AND "+Message.TYPE+"=?", selectionArgs, null, null,null);
if (cursor.getCount() > 0) {
cursor.moveToLast();
do {
Message message = Message.fromCursor(cursor);
message.setConversation(conversation);
list.add(message);
} while (cursor.moveToPrevious());
}
return list;
}
}

View file

@ -14,89 +14,47 @@ import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Base64;
import android.util.Base64OutputStream;
import android.util.Log;
import android.util.LruCache;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.ImageProvider;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.jingle.JingleFile;
import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend {
private static int IMAGE_SIZE = 1920;
private Context context;
private LruCache<String, Bitmap> thumbnailCache;
private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
"yyyyMMdd_HHmmssSSS", Locale.US);
public FileBackend(Context context) {
this.context = context;
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
thumbnailCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
private XmppConnectionService mXmppConnectionService;
public FileBackend(XmppConnectionService service) {
this.mXmppConnectionService = service;
}
public LruCache<String, Bitmap> getThumbnailCache() {
return thumbnailCache;
public DownloadableFile getFile(Message message) {
return getFile(message, true);
}
public JingleFile getJingleFileLegacy(Message message) {
return getJingleFileLegacy(message, true);
}
public JingleFile getJingleFileLegacy(Message message, boolean decrypted) {
Conversation conversation = message.getConversation();
String prefix = context.getFilesDir().getAbsolutePath();
String path = prefix + "/" + conversation.getAccount().getJid() + "/"
+ conversation.getContactJid();
String filename;
if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
filename = message.getUuid() + ".webp";
} else {
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
filename = message.getUuid() + ".webp";
} else {
filename = message.getUuid() + ".webp.pgp";
}
}
return new JingleFile(path + "/" + filename);
}
public JingleFile getJingleFile(Message message) {
return getJingleFile(message, true);
}
public JingleFile getJingleFile(Message message, boolean decrypted) {
public DownloadableFile getFile(Message message, boolean decrypted) {
StringBuilder filename = new StringBuilder();
filename.append(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).getAbsolutePath());
filename.append("/Conversations/");
filename.append(getConversationsDirectory());
filename.append(message.getUuid());
if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
filename.append(".webp");
@ -107,7 +65,13 @@ public class FileBackend {
filename.append(".webp.pgp");
}
}
return new JingleFile(filename.toString());
return new DownloadableFile(filename.toString());
}
public static String getConversationsDirectory() {
return Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES).getAbsolutePath()
+ "/Conversations/";
}
public Bitmap resize(Bitmap originalBitmap, int size) {
@ -139,17 +103,17 @@ public class FileBackend {
return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true);
}
public JingleFile copyImageToPrivateStorage(Message message, Uri image)
public DownloadableFile copyImageToPrivateStorage(Message message, Uri image)
throws ImageCopyException {
return this.copyImageToPrivateStorage(message, image, 0);
}
private JingleFile copyImageToPrivateStorage(Message message, Uri image,
int sampleSize) throws ImageCopyException {
private DownloadableFile copyImageToPrivateStorage(Message message,
Uri image, int sampleSize) throws ImageCopyException {
try {
InputStream is = context.getContentResolver()
InputStream is = mXmppConnectionService.getContentResolver()
.openInputStream(image);
JingleFile file = getJingleFile(message);
DownloadableFile file = getFile(message);
file.getParentFile().mkdirs();
file.createNewFile();
Bitmap originalBitmap;
@ -202,7 +166,7 @@ public class FileBackend {
private int getRotation(Uri image) {
if ("content".equals(image.getScheme())) {
try {
Cursor cursor = context
Cursor cursor = mXmppConnectionService
.getContentResolver()
.query(image,
new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
@ -216,40 +180,26 @@ public class FileBackend {
return -1;
}
} else {
ExifInterface exif;
try {
exif = new ExifInterface(image.toString());
if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
.equalsIgnoreCase("6")) {
return 90;
} else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
.equalsIgnoreCase("8")) {
return 270;
} else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
.equalsIgnoreCase("3")) {
return 180;
} else {
return 0;
}
} catch (IOException e) {
return -1;
InputStream is = mXmppConnectionService.getContentResolver()
.openInputStream(image);
return ExifHelper.getOrientation(is);
} catch (FileNotFoundException e) {
return 0;
}
}
}
public Bitmap getImageFromMessage(Message message) {
return BitmapFactory.decodeFile(getJingleFile(message)
.getAbsolutePath());
return BitmapFactory.decodeFile(getFile(message).getAbsolutePath());
}
public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
throws FileNotFoundException {
Bitmap thumbnail = thumbnailCache.get(message.getUuid());
Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
message.getUuid());
if ((thumbnail == null) && (!cacheOnly)) {
File file = getJingleFile(message);
if (!file.exists()) {
file = getJingleFileLegacy(message);
}
File file = getFile(message);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(file, size);
Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
@ -258,32 +208,12 @@ public class FileBackend {
throw new FileNotFoundException();
}
thumbnail = resize(fullsize, size);
this.thumbnailCache.put(message.getUuid(), thumbnail);
this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
thumbnail);
}
return thumbnail;
}
public void removeFiles(Conversation conversation) {
String prefix = context.getFilesDir().getAbsolutePath();
String path = prefix + "/" + conversation.getAccount().getJid() + "/"
+ conversation.getContactJid();
File file = new File(path);
try {
this.deleteFile(file);
} catch (IOException e) {
Log.d(Config.LOGTAG,
"error deleting file: " + file.getAbsolutePath());
}
}
private void deleteFile(File f) throws IOException {
if (f.isDirectory()) {
for (File c : f.listFiles())
deleteFile(c);
}
f.delete();
}
public Uri getTakePhotoUri() {
StringBuilder pathBuilder = new StringBuilder();
pathBuilder.append(Environment
@ -328,7 +258,7 @@ public class FileBackend {
}
public boolean isAvatarCached(Avatar avatar) {
File file = new File(getAvatarPath(context, avatar.getFilename()));
File file = new File(getAvatarPath(avatar.getFilename()));
return file.exists();
}
@ -336,7 +266,7 @@ public class FileBackend {
if (isAvatarCached(avatar)) {
return true;
}
String filename = getAvatarPath(context, avatar.getFilename());
String filename = getAvatarPath(avatar.getFilename());
File file = new File(filename + ".tmp");
file.getParentFile().mkdirs();
try {
@ -368,15 +298,20 @@ public class FileBackend {
}
}
public static String getAvatarPath(Context context, String avatar) {
return context.getFilesDir().getAbsolutePath() + "/avatars/" + avatar;
public String getAvatarPath(String avatar) {
return mXmppConnectionService.getFilesDir().getAbsolutePath()
+ "/avatars/" + avatar;
}
public Uri getAvatarUri(String avatar) {
return Uri.parse("file:" + getAvatarPath(avatar));
}
public Bitmap cropCenterSquare(Uri image, int size) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(image, size);
InputStream is = context.getContentResolver()
InputStream is = mXmppConnectionService.getContentResolver()
.openInputStream(image);
Bitmap input = BitmapFactory.decodeStream(is, null, options);
if (input == null) {
@ -393,7 +328,40 @@ public class FileBackend {
}
}
public static Bitmap cropCenterSquare(Bitmap input, int size) {
public Bitmap cropCenter(Uri image, int newHeight, int newWidth) {
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(image,
Math.max(newHeight, newWidth));
InputStream is = mXmppConnectionService.getContentResolver()
.openInputStream(image);
Bitmap source = BitmapFactory.decodeStream(is, null, options);
int sourceWidth = source.getWidth();
int sourceHeight = source.getHeight();
float xScale = (float) newWidth / sourceWidth;
float yScale = (float) newHeight / sourceHeight;
float scale = Math.max(xScale, yScale);
float scaledWidth = scale * sourceWidth;
float scaledHeight = scale * sourceHeight;
float left = (newWidth - scaledWidth) / 2;
float top = (newHeight - scaledHeight) / 2;
RectF targetRect = new RectF(left, top, left + scaledWidth, top
+ scaledHeight);
Bitmap dest = Bitmap.createBitmap(newWidth, newHeight,
source.getConfig());
Canvas canvas = new Canvas(dest);
canvas.drawBitmap(source, null, targetRect, null);
return dest;
} catch (FileNotFoundException e) {
return null;
}
}
public Bitmap cropCenterSquare(Bitmap input, int size) {
int w = input.getWidth();
int h = input.getHeight();
@ -415,7 +383,7 @@ public class FileBackend {
throws FileNotFoundException {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(context.getContentResolver()
BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver()
.openInputStream(image), null, options);
return calcSampleSize(options, size);
}
@ -445,12 +413,8 @@ public class FileBackend {
}
public Uri getJingleFileUri(Message message) {
File file = getJingleFile(message);
if (file.exists()) {
return Uri.parse("file://" + file.getAbsolutePath());
} else {
return ImageProvider.getProviderUri(message);
}
File file = getFile(message);
return Uri.parse("file://" + file.getAbsolutePath());
}
public class ImageCopyException extends Exception {
@ -466,12 +430,18 @@ public class FileBackend {
}
}
public static Bitmap getAvatar(String avatar, int size, Context context) {
Bitmap bm = BitmapFactory.decodeFile(FileBackend.getAvatarPath(context,
avatar));
public Bitmap getAvatar(String avatar, int size) {
if (avatar == null) {
return null;
}
Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
if (bm == null) {
return null;
}
return cropCenterSquare(bm, UIHelper.getRealPx(size, context));
return bm;
}
public boolean isFileAvailable(Message message) {
return getFile(message).exists();
}
}

View file

@ -0,0 +1,23 @@
package eu.siacs.conversations.services;
public class AbstractConnectionManager {
protected XmppConnectionService mXmppConnectionService;
public AbstractConnectionManager(XmppConnectionService service) {
this.mXmppConnectionService = service;
}
public XmppConnectionService getXmppConnectionService() {
return this.mXmppConnectionService;
}
public long getAutoAcceptFileSize() {
String config = this.mXmppConnectionService.getPreferences().getString(
"auto_accept_file_size", "524288");
try {
return Long.parseLong(config);
} catch (NumberFormatException e) {
return 524288;
}
}
}

View file

@ -0,0 +1,292 @@
package eu.siacs.conversations.services;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.entities.MucOptions;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.net.Uri;
public class AvatarService {
private static final int FG_COLOR = 0xFFFAFAFA;
private static final int TRANSPARENT = 0x00000000;
private static final String PREFIX_CONTACT = "contact";
private static final String PREFIX_CONVERSATION = "conversation";
private static final String PREFIX_ACCOUNT = "account";
private static final String PREFIX_GENERIC = "generic";
private ArrayList<Integer> sizes = new ArrayList<Integer>();
protected XmppConnectionService mXmppConnectionService = null;
public AvatarService(XmppConnectionService service) {
this.mXmppConnectionService = service;
}
public Bitmap get(Contact contact, int size) {
final String KEY = key(contact, size);
Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY);
if (avatar != null) {
return avatar;
}
avatar = mXmppConnectionService.getFileBackend().getAvatar(
contact.getAvatar(), size);
if (avatar == null) {
if (contact.getProfilePhoto() != null) {
avatar = mXmppConnectionService.getFileBackend()
.cropCenterSquare(Uri.parse(contact.getProfilePhoto()),
size);
if (avatar == null) {
avatar = get(contact.getDisplayName(), size);
}
} else {
avatar = get(contact.getDisplayName(), size);
}
}
this.mXmppConnectionService.getBitmapCache().put(KEY, avatar);
return avatar;
}
public void clear(Contact contact) {
for (Integer size : sizes) {
this.mXmppConnectionService.getBitmapCache().remove(
key(contact, size));
}
}
private String key(Contact contact, int size) {
synchronized (this.sizes) {
if (!this.sizes.contains(size)) {
this.sizes.add(size);
}
}
return PREFIX_CONTACT + "_" + contact.getAccount().getJid() + "_"
+ contact.getJid() + "_" + String.valueOf(size);
}
public Bitmap get(ListItem item, int size) {
if (item instanceof Contact) {
return get((Contact) item, size);
} else if (item instanceof Bookmark) {
Bookmark bookmark = (Bookmark) item;
if (bookmark.getConversation() != null) {
return get(bookmark.getConversation(), size);
} else {
return get(bookmark.getDisplayName(), size);
}
} else {
return get(item.getDisplayName(), size);
}
}
public Bitmap get(Conversation conversation, int size) {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
return get(conversation.getContact(), size);
} else {
return get(conversation.getMucOptions(), size);
}
}
public void clear(Conversation conversation) {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
clear(conversation.getContact());
} else {
clear(conversation.getMucOptions());
}
}
public Bitmap get(MucOptions mucOptions, int size) {
final String KEY = key(mucOptions, size);
Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY);
if (bitmap != null) {
return bitmap;
}
List<MucOptions.User> users = mucOptions.getUsers();
int count = users.size();
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
bitmap.eraseColor(TRANSPARENT);
if (count == 0) {
String name = mucOptions.getConversation().getName();
String letter = name.substring(0, 1);
int color = this.getColorForName(name);
drawTile(canvas, letter, color, 0, 0, size, size);
} else if (count == 1) {
drawTile(canvas, users.get(0), 0, 0, size, size);
} else if (count == 2) {
drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size);
} else if (count == 3) {
drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size);
drawTile(canvas, users.get(1), size / 2 + 1, 0, size, size / 2 - 1);
drawTile(canvas, users.get(2), size / 2 + 1, size / 2 + 1, size,
size);
} else if (count == 4) {
drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
drawTile(canvas, users.get(3), size / 2 + 1, size / 2 + 1, size,
size);
} else {
drawTile(canvas, users.get(0), 0, 0, size / 2 - 1, size / 2 - 1);
drawTile(canvas, users.get(1), 0, size / 2 + 1, size / 2 - 1, size);
drawTile(canvas, users.get(2), size / 2 + 1, 0, size, size / 2 - 1);
drawTile(canvas, "\u2026", 0xFF202020, size / 2 + 1, size / 2 + 1,
size, size);
}
this.mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
return bitmap;
}
public void clear(MucOptions options) {
for (Integer size : sizes) {
this.mXmppConnectionService.getBitmapCache().remove(
key(options, size));
}
}
private String key(MucOptions options, int size) {
synchronized (this.sizes) {
if (!this.sizes.contains(size)) {
this.sizes.add(size);
}
}
return PREFIX_CONVERSATION + "_" + options.getConversation().getUuid()
+ "_" + String.valueOf(size);
}
public Bitmap get(Account account, int size) {
final String KEY = key(account, size);
Bitmap avatar = mXmppConnectionService.getBitmapCache().get(KEY);
if (avatar != null) {
return avatar;
}
avatar = mXmppConnectionService.getFileBackend().getAvatar(
account.getAvatar(), size);
if (avatar == null) {
avatar = get(account.getJid(), size);
}
mXmppConnectionService.getBitmapCache().put(KEY, avatar);
return avatar;
}
public void clear(Account account) {
for (Integer size : sizes) {
this.mXmppConnectionService.getBitmapCache().remove(
key(account, size));
}
}
private String key(Account account, int size) {
synchronized (this.sizes) {
if (!this.sizes.contains(size)) {
this.sizes.add(size);
}
}
return PREFIX_ACCOUNT + "_" + account.getUuid() + "_"
+ String.valueOf(size);
}
public Bitmap get(String name, int size) {
final String KEY = key(name, size);
Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY);
if (bitmap != null) {
return bitmap;
}
bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
String letter = name.substring(0, 1);
int color = this.getColorForName(name);
drawTile(canvas, letter, color, 0, 0, size, size);
mXmppConnectionService.getBitmapCache().put(KEY, bitmap);
return bitmap;
}
private String key(String name, int size) {
synchronized (this.sizes) {
if (!this.sizes.contains(size)) {
this.sizes.add(size);
}
}
return PREFIX_GENERIC + "_" + name + "_" + String.valueOf(size);
}
private void drawTile(Canvas canvas, String letter, int tileColor,
int left, int top, int right, int bottom) {
letter = letter.toUpperCase(Locale.getDefault());
Paint tilePaint = new Paint(), textPaint = new Paint();
tilePaint.setColor(tileColor);
textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(FG_COLOR);
textPaint.setTypeface(Typeface.create("sans-serif-light",
Typeface.NORMAL));
textPaint.setTextSize((float) ((right - left) * 0.8));
Rect rect = new Rect();
canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
textPaint.getTextBounds(letter, 0, 1, rect);
float width = textPaint.measureText(letter);
canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom)
/ 2 + rect.height() / 2, textPaint);
}
private void drawTile(Canvas canvas, MucOptions.User user, int left,
int top, int right, int bottom) {
Contact contact = user.getContact();
if (contact != null) {
Uri uri = null;
if (contact.getAvatar() != null) {
uri = mXmppConnectionService.getFileBackend().getAvatarUri(
contact.getAvatar());
} else if (contact.getProfilePhoto() != null) {
uri = Uri.parse(contact.getProfilePhoto());
}
if (uri != null) {
Bitmap bitmap = mXmppConnectionService.getFileBackend()
.cropCenter(uri, bottom - top, right - left);
if (bitmap != null) {
drawTile(canvas, bitmap, left, top, right, bottom);
} else {
String letter = user.getName().substring(0, 1);
int color = this.getColorForName(user.getName());
drawTile(canvas, letter, color, left, top, right, bottom);
}
} else {
String letter = user.getName().substring(0, 1);
int color = this.getColorForName(user.getName());
drawTile(canvas, letter, color, left, top, right, bottom);
}
} else {
String letter = user.getName().substring(0, 1);
int color = this.getColorForName(user.getName());
drawTile(canvas, letter, color, left, top, right, bottom);
}
}
private void drawTile(Canvas canvas, Bitmap bm, int dstleft, int dsttop,
int dstright, int dstbottom) {
Rect dst = new Rect(dstleft, dsttop, dstright, dstbottom);
canvas.drawBitmap(bm, null, dst, null);
}
private int getColorForName(String name) {
int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5,
0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722,
0xFF795548, 0xFF607d8b };
return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)];
}
}

View file

@ -1,109 +0,0 @@
package eu.siacs.conversations.services;
import java.io.File;
import java.io.FileNotFoundException;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.persistance.DatabaseBackend;
import eu.siacs.conversations.persistance.FileBackend;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
public class ImageProvider extends ContentProvider {
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
ParcelFileDescriptor pfd;
FileBackend fileBackend = new FileBackend(getContext());
if ("r".equals(mode)) {
DatabaseBackend databaseBackend = DatabaseBackend
.getInstance(getContext());
String uuids = uri.getPath();
Log.d(Config.LOGTAG, "uuids = " + uuids + " mode=" + mode);
if (uuids == null) {
throw new FileNotFoundException();
}
String[] uuidsSplited = uuids.split("/", 2);
if (uuidsSplited.length != 3) {
throw new FileNotFoundException();
}
String conversationUuid = uuidsSplited[1];
String messageUuid = uuidsSplited[2].split("\\.")[0];
Log.d(Config.LOGTAG, "messageUuid=" + messageUuid);
Conversation conversation = databaseBackend
.findConversationByUuid(conversationUuid);
if (conversation == null) {
throw new FileNotFoundException("conversation "
+ conversationUuid + " could not be found");
}
Message message = databaseBackend.findMessageByUuid(messageUuid);
if (message == null) {
throw new FileNotFoundException("message " + messageUuid
+ " could not be found");
}
Account account = databaseBackend.findAccountByUuid(conversation
.getAccountUuid());
if (account == null) {
throw new FileNotFoundException("account "
+ conversation.getAccountUuid() + " cound not be found");
}
message.setConversation(conversation);
conversation.setAccount(account);
File file = fileBackend.getJingleFileLegacy(message);
pfd = ParcelFileDescriptor.open(file,
ParcelFileDescriptor.MODE_READ_ONLY);
return pfd;
} else {
throw new FileNotFoundException();
}
}
@Override
public int delete(Uri arg0, String arg1, String[] arg2) {
return 0;
}
@Override
public String getType(Uri arg0) {
return null;
}
@Override
public Uri insert(Uri arg0, ContentValues arg1) {
return null;
}
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri arg0, String[] arg1, String arg2, String[] arg3,
String arg4) {
return null;
}
@Override
public int update(Uri arg0, ContentValues arg1, String arg2, String[] arg3) {
return 0;
}
public static Uri getProviderUri(Message message) {
return Uri.parse("content://eu.siacs.conversations.images/"
+ message.getConversationUuid() + "/" + message.getUuid()
+ ".webp");
}
}

View file

@ -1,5 +1,6 @@
package eu.siacs.conversations.services;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.regex.Matcher;
@ -11,70 +12,83 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.PowerManager;
import android.os.SystemClock;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigPictureStyle;
import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.app.TaskStackBuilder;
import android.text.Html;
import android.util.DisplayMetrics;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ConversationActivity;
public class NotificationService {
private XmppConnectionService mXmppConnectionService;
private NotificationManager mNotificationManager;
private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>();
public int NOTIFICATION_ID = 0x2342;
public static int NOTIFICATION_ID = 0x2342;
private Conversation mOpenConversation;
private boolean mIsInForeground;
private long mEndGracePeriod = 0L;
private long mLastNotification;
public NotificationService(XmppConnectionService service) {
this.mXmppConnectionService = service;
this.mNotificationManager = (NotificationManager) service
.getSystemService(Context.NOTIFICATION_SERVICE);
}
public synchronized void push(Message message) {
public void push(Message message) {
PowerManager pm = (PowerManager) mXmppConnectionService
.getSystemService(Context.POWER_SERVICE);
boolean isScreenOn = pm.isScreenOn();
if (this.mIsInForeground && isScreenOn
&& this.mOpenConversation == message.getConversation()) {
return;
}
String conversationUuid = message.getConversationUuid();
if (notifications.containsKey(conversationUuid)) {
notifications.get(conversationUuid).add(message);
} else {
ArrayList<Message> mList = new ArrayList<Message>();
mList.add(message);
notifications.put(conversationUuid, mList);
synchronized (notifications) {
String conversationUuid = message.getConversationUuid();
if (notifications.containsKey(conversationUuid)) {
notifications.get(conversationUuid).add(message);
} else {
ArrayList<Message> mList = new ArrayList<Message>();
mList.add(message);
notifications.put(conversationUuid, mList);
}
Account account = message.getConversation().getAccount();
updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
&& !account.inGracePeriod()
&& !this.inMiniGracePeriod(account));
}
updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn)
&& !inGracePeriod());
}
public void clear() {
notifications.clear();
updateNotification(false);
synchronized (notifications) {
notifications.clear();
updateNotification(false);
}
}
public void clear(Conversation conversation) {
notifications.remove(conversation.getUuid());
updateNotification(false);
synchronized (notifications) {
notifications.remove(conversation.getUuid());
updateNotification(false);
}
}
private void updateNotification(boolean notify) {
NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
.getSystemService(Context.NOTIFICATION_SERVICE);
SharedPreferences preferences = mXmppConnectionService.getPreferences();
String ringtone = preferences.getString("notification_ringtone", null);
@ -82,76 +96,16 @@ public class NotificationService {
true);
if (notifications.size() == 0) {
mNotificationManager.cancel(NOTIFICATION_ID);
notificationManager.cancel(NOTIFICATION_ID);
} else {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
mXmppConnectionService);
mBuilder.setSmallIcon(R.drawable.ic_notification);
if (notify) {
this.markLastNotification();
}
Builder mBuilder;
if (notifications.size() == 1) {
ArrayList<Message> messages = notifications.values().iterator()
.next();
if (messages.size() >= 1) {
Conversation conversation = messages.get(0)
.getConversation();
mBuilder.setLargeIcon(conversation.getImage(
mXmppConnectionService, 64));
mBuilder.setContentTitle(conversation.getName());
StringBuilder text = new StringBuilder();
for (int i = 0; i < messages.size(); ++i) {
text.append(messages.get(i).getReadableBody(
mXmppConnectionService));
if (i != messages.size() - 1) {
text.append("\n");
}
}
mBuilder.setStyle(new NotificationCompat.BigTextStyle()
.bigText(text.toString()));
mBuilder.setContentText(messages.get(0).getReadableBody(
mXmppConnectionService));
if (notify) {
mBuilder.setTicker(messages.get(messages.size() - 1)
.getReadableBody(mXmppConnectionService));
}
mBuilder.setContentIntent(createContentIntent(conversation
.getUuid()));
} else {
mNotificationManager.cancel(NOTIFICATION_ID);
return;
}
mBuilder = buildSingleConversations(notify);
} else {
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
style.setBigContentTitle(notifications.size()
+ " "
+ mXmppConnectionService
.getString(R.string.unread_conversations));
StringBuilder names = new StringBuilder();
Conversation conversation = null;
for (ArrayList<Message> messages : notifications.values()) {
if (messages.size() > 0) {
conversation = messages.get(0).getConversation();
String name = conversation.getName();
style.addLine(Html.fromHtml("<b>"
+ name
+ "</b> "
+ messages.get(0).getReadableBody(
mXmppConnectionService)));
names.append(name);
names.append(", ");
}
}
if (names.length() >= 2) {
names.delete(names.length() - 2, names.length());
}
mBuilder.setContentTitle(notifications.size()
+ " "
+ mXmppConnectionService
.getString(R.string.unread_conversations));
mBuilder.setContentText(names.toString());
mBuilder.setStyle(style);
if (conversation != null) {
mBuilder.setContentIntent(createContentIntent(conversation
.getUuid()));
}
mBuilder = buildMultipleConversation();
}
if (notify) {
if (vibrate) {
@ -163,12 +117,147 @@ public class NotificationService {
mBuilder.setSound(Uri.parse(ringtone));
}
}
mBuilder.setSmallIcon(R.drawable.ic_notification);
mBuilder.setDeleteIntent(createDeleteIntent());
if (!inGracePeriod()) {
mBuilder.setLights(0xffffffff, 2000, 4000);
}
mBuilder.setLights(0xffffffff, 2000, 4000);
Notification notification = mBuilder.build();
mNotificationManager.notify(NOTIFICATION_ID, notification);
notificationManager.notify(NOTIFICATION_ID, notification);
}
}
private Builder buildMultipleConversation() {
Builder mBuilder = new NotificationCompat.Builder(
mXmppConnectionService);
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle();
style.setBigContentTitle(notifications.size()
+ " "
+ mXmppConnectionService
.getString(R.string.unread_conversations));
StringBuilder names = new StringBuilder();
Conversation conversation = null;
for (ArrayList<Message> messages : notifications.values()) {
if (messages.size() > 0) {
conversation = messages.get(0).getConversation();
String name = conversation.getName();
style.addLine(Html.fromHtml("<b>" + name + "</b> "
+ getReadableBody(messages.get(0))));
names.append(name);
names.append(", ");
}
}
if (names.length() >= 2) {
names.delete(names.length() - 2, names.length());
}
mBuilder.setContentTitle(notifications.size()
+ " "
+ mXmppConnectionService
.getString(R.string.unread_conversations));
mBuilder.setContentText(names.toString());
mBuilder.setStyle(style);
if (conversation != null) {
mBuilder.setContentIntent(createContentIntent(conversation
.getUuid()));
}
return mBuilder;
}
private Builder buildSingleConversations(boolean notify) {
Builder mBuilder = new NotificationCompat.Builder(
mXmppConnectionService);
ArrayList<Message> messages = notifications.values().iterator().next();
if (messages.size() >= 1) {
Conversation conversation = messages.get(0).getConversation();
mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService()
.get(conversation, getPixel(64)));
mBuilder.setContentTitle(conversation.getName());
Message message;
if ((message = getImage(messages)) != null) {
modifyForImage(mBuilder, message, messages, notify);
} else {
modifyForTextOnly(mBuilder, messages, notify);
}
mBuilder.setContentIntent(createContentIntent(conversation
.getUuid()));
}
return mBuilder;
}
private void modifyForImage(Builder builder, Message message,
ArrayList<Message> messages, boolean notify) {
try {
Bitmap bitmap = mXmppConnectionService.getFileBackend()
.getThumbnail(message, getPixel(288), false);
ArrayList<Message> tmp = new ArrayList<Message>();
for (Message msg : messages) {
if (msg.getType() == Message.TYPE_TEXT
&& msg.getDownloadable() == null) {
tmp.add(msg);
}
}
BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle();
bigPictureStyle.bigPicture(bitmap);
if (tmp.size() > 0) {
bigPictureStyle.setSummaryText(getMergedBodies(tmp));
builder.setContentText(getReadableBody(tmp.get(0)));
} else {
builder.setContentText(mXmppConnectionService.getString(R.string.image_file));
}
builder.setStyle(bigPictureStyle);
} catch (FileNotFoundException e) {
modifyForTextOnly(builder, messages, notify);
}
}
private void modifyForTextOnly(Builder builder,
ArrayList<Message> messages, boolean notify) {
builder.setStyle(new NotificationCompat.BigTextStyle()
.bigText(getMergedBodies(messages)));
builder.setContentText(getReadableBody(messages.get(0)));
if (notify) {
builder.setTicker(getReadableBody(messages.get(messages.size() - 1)));
}
}
private Message getImage(ArrayList<Message> messages) {
for (Message message : messages) {
if (message.getType() == Message.TYPE_IMAGE
&& message.getDownloadable() == null
&& message.getEncryption() != Message.ENCRYPTION_PGP) {
return message;
}
}
return null;
}
private String getMergedBodies(ArrayList<Message> messages) {
StringBuilder text = new StringBuilder();
for (int i = 0; i < messages.size(); ++i) {
text.append(getReadableBody(messages.get(i)));
if (i != messages.size() - 1) {
text.append("\n");
}
}
return text.toString();
}
private String getReadableBody(Message message) {
if (message.getDownloadable() != null
&& (message.getDownloadable().getStatus() == Downloadable.STATUS_OFFER || message
.getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) {
return mXmppConnectionService.getText(
R.string.image_offered_for_download).toString();
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
return mXmppConnectionService.getText(
R.string.encrypted_message_received).toString();
} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) {
return mXmppConnectionService.getText(R.string.decryption_failed)
.toString();
} else if (message.getType() == Message.TYPE_IMAGE) {
return mXmppConnectionService.getText(R.string.image_file)
.toString();
} else {
return message.getBody().trim();
}
}
@ -226,16 +315,19 @@ public class NotificationService {
this.mIsInForeground = foreground;
}
public void activateGracePeriod() {
this.mEndGracePeriod = SystemClock.elapsedRealtime()
+ (Config.CARBON_GRACE_PERIOD * 1000);
private int getPixel(int dp) {
DisplayMetrics metrics = mXmppConnectionService.getResources()
.getDisplayMetrics();
return ((int) (dp * metrics.density));
}
public void deactivateGracePeriod() {
this.mEndGracePeriod = 0L;
private void markLastNotification() {
this.mLastNotification = SystemClock.elapsedRealtime();
}
private boolean inGracePeriod() {
return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
private boolean inMiniGracePeriod(Account account) {
int miniGrace = account.getStatus() == Account.STATUS_ONLINE ? Config.MINI_GRACE_PERIOD
: Config.MINI_GRACE_PERIOD * 2;
return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace);
}
}

View file

@ -27,6 +27,7 @@ import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Bookmark;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
@ -34,6 +35,7 @@ import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.generator.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.parser.MessageParser;
import eu.siacs.conversations.parser.PresenceParser;
@ -74,6 +76,7 @@ import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.FileObserver;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
@ -81,11 +84,12 @@ import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.util.Log;
import android.util.LruCache;
public class XmppConnectionService extends Service {
public DatabaseBackend databaseBackend;
private FileBackend fileBackend;
private FileBackend fileBackend = new FileBackend(this);
public long startDate;
@ -94,7 +98,8 @@ public class XmppConnectionService extends Service {
private MemorizingTrustManager mMemorizingTrustManager;
private NotificationService mNotificationService;
private NotificationService mNotificationService = new NotificationService(
this);
private MessageParser mMessageParser = new MessageParser(this);
private PresenceParser mPresenceParser = new PresenceParser(this);
@ -106,20 +111,27 @@ public class XmppConnectionService extends Service {
private CopyOnWriteArrayList<Conversation> conversations = null;
private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
this);
private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
this);
private AvatarService mAvatarService = new AvatarService(this);
private OnConversationUpdate mOnConversationUpdate = null;
private int convChangedListenerCount = 0;
private Integer convChangedListenerCount = 0;
private OnAccountUpdate mOnAccountUpdate = null;
private int accountChangedListenerCount = 0;
private Integer accountChangedListenerCount = 0;
private OnRosterUpdate mOnRosterUpdate = null;
private int rosterChangedListenerCount = 0;
private Integer rosterChangedListenerCount = 0;
public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
@Override
public void onContactStatusChanged(Contact contact, boolean online) {
Conversation conversation = find(getConversations(), contact);
if (conversation != null) {
conversation.endOtrIfNeeded();
if (online && contact.getPresences().size() > 1) {
conversation.endOtrIfNeeded();
} else {
conversation.resetOtrSession();
}
if (online && (contact.getPresences().size() == 1)) {
sendUnsendMessages(conversation);
}
@ -140,6 +152,17 @@ public class XmppConnectionService extends Service {
}
};
private FileObserver fileObserver = new FileObserver(
FileBackend.getConversationsDirectory()) {
@Override
public void onEvent(int event, String path) {
if (event == FileObserver.DELETE) {
markFileDeleted(path.split("\\.")[0]);
}
}
};
private final IBinder mBinder = new XmppConnectionBinder();
private OnStatusChanged statusListener = new OnStatusChanged() {
@ -252,6 +275,7 @@ public class XmppConnectionService extends Service {
}
}
};
private LruCache<String, Bitmap> mBitmapCache;
public PgpEngine getPgpEngine() {
if (pgpServiceConnection.isBound()) {
@ -271,6 +295,10 @@ public class XmppConnectionService extends Service {
return this.fileBackend;
}
public AvatarService getAvatarService() {
return this.mAvatarService;
}
public Message attachImageToConversation(final Conversation conversation,
final Uri uri, final UiCallback<Message> callback) {
final Message message;
@ -331,15 +359,10 @@ public class XmppConnectionService extends Service {
}
}
this.wakeLock.acquire();
ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
boolean isConnected = activeNetwork != null
&& activeNetwork.isConnected();
for (Account account : accounts) {
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
if (!isConnected) {
if (!hasInternetConnection()) {
account.setStatus(Account.STATUS_NO_INTERNET);
if (statusListener != null) {
statusListener.onStatusChanged(account);
@ -398,6 +421,13 @@ public class XmppConnectionService extends Service {
return START_STICKY;
}
public boolean hasInternetConnection() {
ConnectivityManager cm = (ConnectivityManager) getApplicationContext()
.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo activeNetwork = cm.getActiveNetworkInfo();
return activeNetwork != null && activeNetwork.isConnected();
}
@SuppressLint("TrulyRandom")
@Override
public void onCreate() {
@ -406,10 +436,18 @@ public class XmppConnectionService extends Service {
this.mRandom = new SecureRandom();
this.mMemorizingTrustManager = new MemorizingTrustManager(
getApplicationContext());
this.mNotificationService = new NotificationService(this);
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
this.databaseBackend = DatabaseBackend
.getInstance(getApplicationContext());
this.fileBackend = new FileBackend(getApplicationContext());
this.accounts = databaseBackend.getAccounts();
for (Account account : this.accounts) {
@ -420,6 +458,7 @@ public class XmppConnectionService extends Service {
getContentResolver().registerContentObserver(
ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
this.fileObserver.startWatching();
this.pgpServiceConnection = new OpenPgpServiceConnection(
getApplicationContext(), "org.sufficientlysecure.keychain");
this.pgpServiceConnection.bindToService();
@ -511,8 +550,9 @@ public class XmppConnectionService extends Service {
return connection;
}
synchronized public void sendMessage(Message message) {
public void sendMessage(Message message) {
Account account = message.getConversation().getAccount();
account.deactivateGracePeriod();
Conversation conv = message.getConversation();
MessagePacket packet = null;
boolean saveInDb = true;
@ -531,13 +571,14 @@ public class XmppConnectionService extends Service {
&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
mJingleConnectionManager
.createNewConnection(message);
} else if (message.getPresence() == null) {
message.setStatus(Message.STATUS_WAITING);
}
} else {
mJingleConnectionManager.createNewConnection(message);
}
} else {
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
conv.startOtrIfNeeded();
}
message.setStatus(Message.STATUS_WAITING);
}
} else {
@ -554,6 +595,7 @@ public class XmppConnectionService extends Service {
send = true;
} else if (message.getPresence() == null) {
conv.startOtrIfNeeded();
message.setStatus(Message.STATUS_WAITING);
}
} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) {
@ -596,7 +638,7 @@ public class XmppConnectionService extends Service {
}
}
conv.getMessages().add(message);
conv.add(message);
if (saveInDb) {
if (message.getEncryption() == Message.ENCRYPTION_NONE
|| saveEncryptedMessages()) {
@ -776,6 +818,7 @@ public class XmppConnectionService extends Service {
.getString("photouri"));
contact.setSystemName(phoneContact
.getString("displayname"));
getAvatarService().clear(contact);
}
}
}
@ -794,12 +837,39 @@ public class XmppConnectionService extends Service {
Account account = accountLookupTable.get(conv.getAccountUuid());
conv.setAccount(account);
conv.setMessages(databaseBackend.getMessages(conv, 50));
checkDeletedFiles(conv);
}
}
return this.conversations;
}
private void checkDeletedFiles(Conversation conversation) {
for (Message message : conversation.getMessages()) {
if (message.getType() == Message.TYPE_IMAGE
&& message.getEncryption() != Message.ENCRYPTION_PGP) {
if (!getFileBackend().isFileAvailable(message)) {
message.setDownloadable(new DeletedDownloadable());
}
}
}
}
private void markFileDeleted(String uuid) {
for (Conversation conversation : getConversations()) {
for (Message message : conversation.getMessages()) {
if (message.getType() == Message.TYPE_IMAGE
&& message.getEncryption() != Message.ENCRYPTION_PGP
&& message.getUuid().equals(uuid)) {
if (!getFileBackend().isFileAvailable(message)) {
message.setDownloadable(new DeletedDownloadable());
updateConversationUi();
}
return;
}
}
}
}
public void populateWithOrderedConversations(List<Conversation> list) {
populateWithOrderedConversations(list, true);
}
@ -838,7 +908,7 @@ public class XmppConnectionService extends Service {
for (Message message : messages) {
message.setConversation(conversation);
}
conversation.getMessages().addAll(0, messages);
conversation.addAll(0, messages);
return messages.size();
}
@ -858,9 +928,9 @@ public class XmppConnectionService extends Service {
public Conversation find(List<Conversation> haystack, Account account,
String jid) {
for (Conversation conversation : haystack) {
if ((account == null || conversation.getAccount().equals(account))
if ((account == null || conversation.getAccount() == account)
&& (conversation.getContactJid().split("/", 2)[0]
.equals(jid))) {
.equalsIgnoreCase(jid))) {
return conversation;
}
}
@ -927,7 +997,6 @@ public class XmppConnectionService extends Service {
public void clearConversationHistory(Conversation conversation) {
this.databaseBackend.deleteMessagesInConversation(conversation);
this.fileBackend.removeFiles(conversation);
conversation.getMessages().clear();
updateConversationUi();
}
@ -973,60 +1042,85 @@ public class XmppConnectionService extends Service {
public void setOnConversationListChangedListener(
OnConversationUpdate listener) {
this.mNotificationService.deactivateGracePeriod();
if (checkListeners()) {
switchToForeground();
if (!isScreenOn()) {
Log.d(Config.LOGTAG,
"ignoring setOnConversationListChangedListener");
return;
}
synchronized (this.convChangedListenerCount) {
if (checkListeners()) {
switchToForeground();
}
this.mOnConversationUpdate = listener;
this.mNotificationService.setIsInForeground(true);
this.convChangedListenerCount++;
}
this.mOnConversationUpdate = listener;
this.mNotificationService.setIsInForeground(true);
this.convChangedListenerCount++;
}
public void removeOnConversationListChangedListener() {
this.convChangedListenerCount--;
if (this.convChangedListenerCount == 0) {
this.mOnConversationUpdate = null;
this.mNotificationService.setIsInForeground(false);
if (checkListeners()) {
switchToBackground();
synchronized (this.convChangedListenerCount) {
this.convChangedListenerCount--;
if (this.convChangedListenerCount <= 0) {
this.convChangedListenerCount = 0;
this.mOnConversationUpdate = null;
this.mNotificationService.setIsInForeground(false);
if (checkListeners()) {
switchToBackground();
}
}
}
}
public void setOnAccountListChangedListener(OnAccountUpdate listener) {
this.mNotificationService.deactivateGracePeriod();
if (checkListeners()) {
switchToForeground();
if (!isScreenOn()) {
Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener");
return;
}
synchronized (this.accountChangedListenerCount) {
if (checkListeners()) {
switchToForeground();
}
this.mOnAccountUpdate = listener;
this.accountChangedListenerCount++;
}
this.mOnAccountUpdate = listener;
this.accountChangedListenerCount++;
}
public void removeOnAccountListChangedListener() {
this.accountChangedListenerCount--;
if (this.accountChangedListenerCount == 0) {
this.mOnAccountUpdate = null;
if (checkListeners()) {
switchToBackground();
synchronized (this.accountChangedListenerCount) {
this.accountChangedListenerCount--;
if (this.accountChangedListenerCount <= 0) {
this.mOnAccountUpdate = null;
this.accountChangedListenerCount = 0;
if (checkListeners()) {
switchToBackground();
}
}
}
}
public void setOnRosterUpdateListener(OnRosterUpdate listener) {
this.mNotificationService.deactivateGracePeriod();
if (checkListeners()) {
switchToForeground();
if (!isScreenOn()) {
Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener");
return;
}
synchronized (this.rosterChangedListenerCount) {
if (checkListeners()) {
switchToForeground();
}
this.mOnRosterUpdate = listener;
this.rosterChangedListenerCount++;
}
this.mOnRosterUpdate = listener;
this.rosterChangedListenerCount++;
}
public void removeOnRosterUpdateListener() {
this.rosterChangedListenerCount--;
if (this.rosterChangedListenerCount == 0) {
this.mOnRosterUpdate = null;
if (checkListeners()) {
switchToBackground();
synchronized (this.rosterChangedListenerCount) {
this.rosterChangedListenerCount--;
if (this.rosterChangedListenerCount <= 0) {
this.rosterChangedListenerCount = 0;
this.mOnRosterUpdate = null;
if (checkListeners()) {
switchToBackground();
}
}
}
}
@ -1042,11 +1136,10 @@ public class XmppConnectionService extends Service {
XmppConnection connection = account.getXmppConnection();
if (connection != null && connection.getFeatures().csi()) {
connection.sendActive();
Log.d(Config.LOGTAG, account.getJid()
+ " sending csi//active");
}
}
}
Log.d(Config.LOGTAG, "app switched into foreground");
}
private void switchToBackground() {
@ -1055,11 +1148,17 @@ public class XmppConnectionService extends Service {
XmppConnection connection = account.getXmppConnection();
if (connection != null && connection.getFeatures().csi()) {
connection.sendInactive();
Log.d(Config.LOGTAG, account.getJid()
+ " sending csi//inactive");
}
}
}
this.mNotificationService.setIsInForeground(false);
Log.d(Config.LOGTAG, "app switched into background");
}
private boolean isScreenOn() {
PowerManager pm = (PowerManager) this
.getSystemService(Context.POWER_SERVICE);
return pm.isScreenOn();
}
public void connectMultiModeConversations(Account account) {
@ -1197,7 +1296,7 @@ public class XmppConnectionService extends Service {
conversation.getMucOptions().setOffline();
conversation.deregisterWithBookmark();
Log.d(Config.LOGTAG, conversation.getAccount().getJid()
+ " leaving muc " + conversation.getContactJid());
+ ": leaving muc " + conversation.getContactJid());
} else {
account.pendingConferenceLeaves.add(conversation);
}
@ -1214,7 +1313,11 @@ public class XmppConnectionService extends Service {
if (conversation.getMode() == Conversation.MODE_MULTI) {
leaveMuc(conversation);
} else {
conversation.endOtrIfNeeded();
if (conversation.endOtrIfNeeded()) {
Log.d(Config.LOGTAG, account.getJid()
+ ": ended otr session with "
+ conversation.getContactJid());
}
}
}
}
@ -1230,6 +1333,7 @@ public class XmppConnectionService extends Service {
public void updateMessage(Message message) {
databaseBackend.updateMessage(message);
updateConversationUi();
}
protected void syncDirtyContacts(Account account) {
@ -1410,10 +1514,16 @@ public class XmppConnectionService extends Service {
if (account.setAvatar(avatar.getFilename())) {
databaseBackend.updateAccount(account);
}
getAvatarService().clear(account);
updateConversationUi();
updateAccountUi();
} else {
Contact contact = account.getRoster()
.getContact(avatar.owner);
contact.setAvatar(avatar.getFilename());
getAvatarService().clear(contact);
updateConversationUi();
updateRosterUi();
}
if (callback != null) {
callback.success(avatar);
@ -1463,6 +1573,7 @@ public class XmppConnectionService extends Service {
if (account.setAvatar(avatar.getFilename())) {
databaseBackend.updateAccount(account);
}
getAvatarService().clear(account);
callback.success(avatar);
} else {
fetchAvatar(account, avatar, callback);
@ -1675,6 +1786,10 @@ public class XmppConnectionService extends Service {
return this.pm;
}
public LruCache<String, Bitmap> getBitmapCache() {
return this.mBitmapCache;
}
public void replyWithNotAcceptable(Account account, MessagePacket packet) {
if (account.getStatus() == Account.STATUS_ONLINE) {
MessagePacket error = this.mMessageGenerator
@ -1779,8 +1894,7 @@ public class XmppConnectionService extends Service {
ArrayList<Contact> contacts = new ArrayList<Contact>();
for (Account account : getAccounts()) {
if (!account.isOptionSet(Account.OPTION_DISABLED)) {
Contact contact = account.getRoster()
.getContactAsShownInRoster(jid);
Contact contact = account.getRoster().getContactFromRoster(jid);
if (contact != null) {
contacts.add(contact);
}
@ -1792,4 +1906,44 @@ public class XmppConnectionService extends Service {
public NotificationService getNotificationService() {
return this.mNotificationService;
}
public HttpConnectionManager getHttpConnectionManager() {
return this.mHttpConnectionManager;
}
private class DeletedDownloadable implements Downloadable {
@Override
public boolean start() {
return false;
}
@Override
public int getStatus() {
return Downloadable.STATUS_DELETED;
}
@Override
public long getFileSize() {
return 0;
}
}
public void resendFailedMessages(Message message) {
List<Message> messages = new ArrayList<Message>();
Message current = message;
while(current.getStatus() == Message.STATUS_SEND_FAILED) {
messages.add(current);
if (current.mergable(current.next())) {
current = current.next();
} else {
break;
}
}
for(Message msg: messages) {
markMessage(msg, Message.STATUS_WAITING);
this.resendMessage(msg);
}
}
}

View file

@ -113,7 +113,7 @@ public class ChooseContactActivity extends XmppActivity {
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.choose_contact, menu);
MenuItem menuSearchView = (MenuItem) menu.findItem(R.id.action_search);
MenuItem menuSearchView = menu.findItem(R.id.action_search);
View mSearchView = menuSearchView.getActionView();
mSearchEditText = (EditText) mSearchView
.findViewById(R.id.search_field);

View file

@ -7,14 +7,12 @@ import org.openintents.openpgp.util.OpenPgpUtils;
import eu.siacs.conversations.R;
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.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import android.app.PendingIntent;
import android.content.Context;
@ -41,6 +39,7 @@ public class ConferenceDetailsActivity extends XmppActivity {
private ImageButton mEditNickButton;
private TextView mRoleAffiliaton;
private TextView mFullJid;
private TextView mAccountJid;
private LinearLayout membersView;
private LinearLayout mMoreDetails;
private Button mInviteButton;
@ -78,6 +77,7 @@ public class ConferenceDetailsActivity extends XmppActivity {
mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button);
mFullJid = (TextView) findViewById(R.id.muc_jabberid);
membersView = (LinearLayout) findViewById(R.id.muc_members);
mAccountJid = (TextView) findViewById(R.id.details_account);
mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details);
mMoreDetails.setVisibility(View.GONE);
mInviteButton = (Button) findViewById(R.id.invite);
@ -169,37 +169,38 @@ public class ConferenceDetailsActivity extends XmppActivity {
}
protected void registerListener() {
if (xmppConnectionServiceBound) {
xmppConnectionService
.setOnConversationListChangedListener(this.onConvChanged);
xmppConnectionService.setOnRenameListener(new OnRenameListener() {
xmppConnectionService
.setOnConversationListChangedListener(this.onConvChanged);
xmppConnectionService.setOnRenameListener(new OnRenameListener() {
@Override
public void onRename(final boolean success) {
runOnUiThread(new Runnable() {
@Override
public void onRename(final boolean success) {
runOnUiThread(new Runnable() {
@Override
public void run() {
populateView();
if (success) {
Toast.makeText(
ConferenceDetailsActivity.this,
getString(R.string.your_nick_has_been_changed),
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(ConferenceDetailsActivity.this,
getString(R.string.nick_in_use),
Toast.LENGTH_SHORT).show();
}
@Override
public void run() {
populateView();
if (success) {
Toast.makeText(
ConferenceDetailsActivity.this,
getString(R.string.your_nick_has_been_changed),
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(ConferenceDetailsActivity.this,
getString(R.string.nick_in_use),
Toast.LENGTH_SHORT).show();
}
});
}
});
}
}
});
}
});
}
private void populateView() {
mYourPhoto.setImageBitmap(conversation.getAccount().getImage(this, 48));
mAccountJid.setText(getString(R.string.using_account, conversation
.getAccount().getJid()));
mYourPhoto.setImageBitmap(avatarService().get(
conversation.getAccount(), getPixel(48)));
setTitle(conversation.getName());
mFullJid.setText(conversation.getContactJid().split("/", 2)[0]);
mYourNick.setText(conversation.getMucOptions().getActualNick());
@ -225,9 +226,8 @@ public class ConferenceDetailsActivity extends XmppActivity {
this.users.addAll(conversation.getMucOptions().getUsers());
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
membersView.removeAllViews();
Account account = conversation.getAccount();
for (final User user : conversation.getMucOptions().getUsers()) {
View view = (View) inflater.inflate(R.layout.contact, membersView,
View view = inflater.inflate(R.layout.contact, membersView,
false);
TextView name = (TextView) view
.findViewById(R.id.contact_display_name);
@ -245,22 +245,14 @@ public class ConferenceDetailsActivity extends XmppActivity {
key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
}
Bitmap bm;
if (user.getJid() != null) {
Contact contact = account.getRoster().getContact(user.getJid());
if (contact.showInRoster()) {
bm = contact.getImage(48, this);
name.setText(contact.getDisplayName());
role.setText(user.getName() + " \u2022 "
+ getReadableRole(user.getRole()));
} else {
bm = UIHelper.getContactPicture(user.getName(), 48, this,
false);
name.setText(user.getName());
role.setText(getReadableRole(user.getRole()));
}
Contact contact = user.getContact();
if (contact != null) {
bm = avatarService().get(contact, getPixel(48));
name.setText(contact.getDisplayName());
role.setText(user.getName() + " \u2022 "
+ getReadableRole(user.getRole()));
} else {
bm = UIHelper
.getContactPicture(user.getName(), 48, this, false);
bm = avatarService().get(user.getName(), getPixel(48));
name.setText(user.getName());
role.setText(getReadableRole(user.getRole()));
}

View file

@ -56,7 +56,8 @@ public class ContactDetailsActivity extends XmppActivity {
@Override
public void onClick(DialogInterface dialog, int which) {
ContactDetailsActivity.this.xmppConnectionService.deleteContactOnServer(contact);
ContactDetailsActivity.this.xmppConnectionService
.deleteContactOnServer(contact);
ContactDetailsActivity.this.finish();
}
};
@ -78,7 +79,8 @@ public class ContactDetailsActivity extends XmppActivity {
@Override
public void onClick(View v) {
AlertDialog.Builder builder = new AlertDialog.Builder(ContactDetailsActivity.this);
AlertDialog.Builder builder = new AlertDialog.Builder(
ContactDetailsActivity.this);
builder.setTitle(getString(R.string.action_add_phone_book));
builder.setMessage(getString(R.string.add_phone_book_text,
contact.getJid()));
@ -309,22 +311,21 @@ public class ContactDetailsActivity extends XmppActivity {
} else {
contactJidTv.setText(contact.getJid());
}
accountJidTv.setText(contact.getAccount().getJid());
UIHelper.prepareContactBadge(this, badge, contact,
getApplicationContext());
accountJidTv.setText(getString(R.string.using_account, contact
.getAccount().getJid()));
prepareContactBadge(badge, contact);
if (contact.getSystemAccount() == null) {
badge.setOnClickListener(onBadgeClick);
}
keys.removeAllViews();
boolean hasKeys = false;
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
for (Iterator<String> iterator = contact.getOtrFingerprints()
.iterator(); iterator.hasNext();) {
hasKeys = true;
final String otrFingerprint = iterator.next();
View view = (View) inflater.inflate(R.layout.contact_key, keys,
false);
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
@ -342,8 +343,8 @@ public class ContactDetailsActivity extends XmppActivity {
});
}
if (contact.getPgpKeyId() != 0) {
View view = (View) inflater.inflate(R.layout.contact_key, keys,
false);
hasKeys = true;
View view = inflater.inflate(R.layout.contact_key, keys, false);
TextView key = (TextView) view.findViewById(R.id.key);
TextView keyType = (TextView) view.findViewById(R.id.key_type);
keyType.setText("PGP Key ID");
@ -370,6 +371,20 @@ public class ContactDetailsActivity extends XmppActivity {
});
keys.addView(view);
}
if (hasKeys) {
keys.setVisibility(View.VISIBLE);
} else {
keys.setVisibility(View.GONE);
}
}
private void prepareContactBadge(QuickContactBadge badge, Contact contact) {
if (contact.getSystemAccount() != null) {
String[] systemAccount = contact.getSystemAccount().split("#");
long id = Long.parseLong(systemAccount[0]);
badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1]));
}
badge.setImageBitmap(avatarService().get(contact, getPixel(72)));
}
protected void confirmToDeleteFingerprint(final String fingerprint) {

View file

@ -191,6 +191,7 @@ public class ConversationActivity extends XmppActivity implements
xmppConnectionService.getNotificationService()
.setOpenConversation(null);
}
closeContextMenu();
}
@Override
@ -222,7 +223,8 @@ public class ConversationActivity extends XmppActivity implements
ab.setDisplayHomeAsUpEnabled(true);
ab.setHomeButtonEnabled(true);
if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE
|| ConversationActivity.this.useSubjectToIdentifyConference()) {
|| ConversationActivity.this
.useSubjectToIdentifyConference()) {
ab.setTitle(getSelectedConversation().getName());
} else {
ab.setTitle(getSelectedConversation().getContactJid()
@ -239,19 +241,16 @@ public class ConversationActivity extends XmppActivity implements
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.conversations, menu);
MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security);
MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive);
MenuItem menuMucDetails = (MenuItem) menu
.findItem(R.id.action_muc_details);
MenuItem menuContactDetails = (MenuItem) menu
MenuItem menuSecure = menu.findItem(R.id.action_security);
MenuItem menuArchive = menu.findItem(R.id.action_archive);
MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
MenuItem menuContactDetails = menu
.findItem(R.id.action_contact_details);
MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file);
MenuItem menuClearHistory = (MenuItem) menu
.findItem(R.id.action_clear_history);
MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add);
MenuItem menuInviteContact = (MenuItem) menu
.findItem(R.id.action_invite);
MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute);
MenuItem menuAttach = menu.findItem(R.id.action_attach_file);
MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history);
MenuItem menuAdd = menu.findItem(R.id.action_add);
MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
MenuItem menuMute = menu.findItem(R.id.action_mute);
if (isConversationsOverviewVisable()
&& isConversationsOverviewHideable()) {
@ -604,8 +603,11 @@ public class ConversationActivity extends XmppActivity implements
.beginTransaction();
transaction.replace(R.id.selected_conversation, selectedFragment,
"conversation");
transaction.commitAllowingStateLoss();
try {
transaction.commitAllowingStateLoss();
} catch (IllegalStateException e) {
return selectedFragment;
}
}
return selectedFragment;
}
@ -624,23 +626,10 @@ public class ConversationActivity extends XmppActivity implements
@Override
protected void onNewIntent(Intent intent) {
if (xmppConnectionServiceBound) {
if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION
.equals(intent.getType())))) {
String convToView = (String) intent.getExtras().get(
CONVERSATION);
updateConversationList();
for (int i = 0; i < conversationList.size(); ++i) {
if (conversationList.get(i).getUuid().equals(convToView)) {
setSelectedConversation(conversationList.get(i));
break;
}
}
paneShouldBeOpen = false;
String text = intent.getExtras().getString(TEXT, null);
swapConversationFragment().setText(text);
if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) {
handleViewConversationIntent(intent);
}
} else {
handledViewIntent = false;
setIntent(intent);
}
}
@ -690,6 +679,10 @@ public class ConversationActivity extends XmppActivity implements
} else if (conversationList.size() <= 0) {
startActivity(new Intent(this, StartConversationActivity.class));
finish();
} else if (getIntent() != null
&& VIEW_CONVERSATION.equals(getIntent().getType())) {
handleViewConversationIntent(getIntent());
setIntent(null);
} else if (mOpenConverstaion != null) {
selectConversationByUuid(mOpenConverstaion);
paneShouldBeOpen = mPanelOpen;
@ -698,14 +691,6 @@ public class ConversationActivity extends XmppActivity implements
}
swapConversationFragment();
mOpenConverstaion = null;
} else if (getIntent() != null
&& VIEW_CONVERSATION.equals(getIntent().getType())) {
String uuid = (String) getIntent().getExtras().get(CONVERSATION);
String text = getIntent().getExtras().getString(TEXT, null);
selectConversationByUuid(uuid);
paneShouldBeOpen = false;
swapConversationFragment().setText(text);
setIntent(null);
} else {
showConversationsOverview();
ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
@ -727,6 +712,14 @@ public class ConversationActivity extends XmppActivity implements
ExceptionHelper.checkForCrash(this, this.xmppConnectionService);
}
private void handleViewConversationIntent(Intent intent) {
String uuid = (String) intent.getExtras().get(CONVERSATION);
String text = intent.getExtras().getString(TEXT, null);
selectConversationByUuid(uuid);
paneShouldBeOpen = false;
swapConversationFragment().setText(text);
}
private void selectConversationByUuid(String uuid) {
for (int i = 0; i < conversationList.size(); ++i) {
if (conversationList.get(i).getUuid().equals(uuid)) {
@ -736,11 +729,9 @@ public class ConversationActivity extends XmppActivity implements
}
public void registerListener() {
if (xmppConnectionServiceBound) {
xmppConnectionService.setOnConversationListChangedListener(this);
xmppConnectionService.setOnAccountListChangedListener(this);
xmppConnectionService.setOnRosterUpdateListener(this);
}
xmppConnectionService.setOnConversationListChangedListener(this);
xmppConnectionService.setOnAccountListChangedListener(this);
xmppConnectionService.setOnRosterUpdateListener(this);
}
@Override
@ -753,6 +744,7 @@ public class ConversationActivity extends XmppActivity implements
.findFragmentByTag("conversation");
if (selectedFragment != null) {
selectedFragment.hideSnackbar();
selectedFragment.updateMessages();
}
} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
pendingImageUri = data.getData();
@ -786,6 +778,10 @@ public class ConversationActivity extends XmppActivity implements
attachAudioToConversation(getSelectedConversation(),
data.getData());
}
} else {
if (requestCode == REQUEST_IMAGE_CAPTURE) {
pendingImageUri = null;
}
}
}

View file

@ -3,6 +3,7 @@ package eu.siacs.conversations.ui;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import net.java.otr4j.session.SessionStatus;
import eu.siacs.conversations.R;
@ -32,9 +33,12 @@ import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.text.Editable;
import android.text.Selection;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
@ -44,6 +48,8 @@ import android.widget.AbsListView.OnScrollListener;
import android.widget.TextView.OnEditorActionListener;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ListView;
import android.widget.ImageButton;
import android.widget.RelativeLayout;
@ -72,6 +78,9 @@ public class ConversationFragment extends Fragment {
private IntentSender askForPassphraseIntent = null;
private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<Message>();
private boolean mDecryptJobRunning = false;
private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
@Override
@ -189,6 +198,7 @@ public class ConversationFragment extends Fragment {
};
private ConversationActivity activity;
private Message selectedMessage;
private void sendMessage() {
if (this.conversation == null) {
@ -322,9 +332,114 @@ public class ConversationFragment extends Fragment {
});
messagesView.setAdapter(messageListAdapter);
registerForContextMenu(messagesView);
return view;
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
this.selectedMessage = this.messageList.get(acmi.position);
populateContextMenu(menu);
}
private void populateContextMenu(ContextMenu menu) {
if (this.selectedMessage.getType() != Message.TYPE_STATUS) {
activity.getMenuInflater().inflate(R.menu.message_context, menu);
menu.setHeaderTitle(R.string.message_options);
MenuItem copyText = menu.findItem(R.id.copy_text);
MenuItem shareImage = menu.findItem(R.id.share_image);
MenuItem sendAgain = menu.findItem(R.id.send_again);
MenuItem copyUrl = menu.findItem(R.id.copy_url);
MenuItem downloadImage = menu.findItem(R.id.download_image);
if (this.selectedMessage.getType() != Message.TYPE_TEXT
|| this.selectedMessage.getDownloadable() != null) {
copyText.setVisible(false);
}
if (this.selectedMessage.getType() != Message.TYPE_IMAGE
|| this.selectedMessage.getDownloadable() != null) {
shareImage.setVisible(false);
}
if (this.selectedMessage.getStatus() != Message.STATUS_SEND_FAILED) {
sendAgain.setVisible(false);
}
if ((this.selectedMessage.getType() != Message.TYPE_IMAGE && this.selectedMessage
.getDownloadable() == null)
|| this.selectedMessage.getImageParams().url == null) {
copyUrl.setVisible(false);
}
if (this.selectedMessage.getType() != Message.TYPE_TEXT
|| this.selectedMessage.getDownloadable() != null
|| !this.selectedMessage.bodyContainsDownloadable()) {
downloadImage.setVisible(false);
}
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.share_image:
shareImage(selectedMessage);
return true;
case R.id.copy_text:
copyText(selectedMessage);
return true;
case R.id.send_again:
resendMessage(selectedMessage);
return true;
case R.id.copy_url:
copyUrl(selectedMessage);
return true;
case R.id.download_image:
downloadImage(selectedMessage);
return true;
default:
return super.onContextItemSelected(item);
}
}
private void shareImage(Message message) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM,
activity.xmppConnectionService.getFileBackend()
.getJingleFileUri(message));
shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
shareIntent.setType("image/webp");
activity.startActivity(Intent.createChooser(shareIntent,
getText(R.string.share_with)));
}
private void copyText(Message message) {
if (activity.copyTextToClipboard(message.getMergedBody(),
R.string.message_text)) {
Toast.makeText(activity, R.string.message_copied_to_clipboard,
Toast.LENGTH_SHORT).show();
}
}
private void resendMessage(Message message) {
activity.xmppConnectionService.resendFailedMessages(message);
}
private void copyUrl(Message message) {
if (activity.copyTextToClipboard(
message.getImageParams().url.toString(), R.string.image_url)) {
Toast.makeText(activity, R.string.url_copied_to_clipboard,
Toast.LENGTH_SHORT).show();
}
}
private void downloadImage(Message message) {
activity.xmppConnectionService.getHttpConnectionManager()
.createNewConnection(message);
}
protected void privateMessageWith(String counterpart) {
this.mEditMessage.setText("");
this.conversation.setNextPresence(counterpart);
@ -356,6 +471,7 @@ public class ConversationFragment extends Fragment {
@Override
public void onStop() {
mDecryptJobRunning = false;
super.onStop();
if (this.conversation != null) {
this.conversation.setNextMessage(mEditMessage.getText().toString());
@ -395,34 +511,6 @@ public class ConversationFragment extends Fragment {
updateMessages();
}
private void decryptMessage(Message message) {
PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
if (engine != null) {
engine.decrypt(message, new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message message) {
askForPassphraseIntent = pi.getIntentSender();
showSnackbar(R.string.openpgp_messages_found,
R.string.decrypt, clickToDecryptListener);
}
@Override
public void success(Message message) {
activity.xmppConnectionService.databaseBackend
.updateMessage(message);
updateMessages();
}
@Override
public void error(int error, Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
// updateMessages();
}
});
}
}
public void updateMessages() {
if (getView() == null) {
return;
@ -458,13 +546,16 @@ public class ConversationFragment extends Fragment {
});
}
for (Message message : this.conversation.getMessages()) {
if ((message.getEncryption() == Message.ENCRYPTION_PGP)
&& ((message.getStatus() == Message.STATUS_RECEIVED) || (message
.getStatus() == Message.STATUS_SEND))) {
decryptMessage(message);
break;
if (message.getEncryption() == Message.ENCRYPTION_PGP
&& (message.getStatus() == Message.STATUS_RECEIVED || message
.getStatus() >= Message.STATUS_SEND)
&& message.getDownloadable() == null) {
if (!mEncryptedMessages.contains(message)) {
mEncryptedMessages.add(message);
}
}
}
decryptNext();
this.messageList.clear();
if (this.conversation.getMessages().size() == 0) {
messagesLoaded = false;
@ -476,7 +567,7 @@ public class ConversationFragment extends Fragment {
this.messageListAdapter.notifyDataSetChanged();
if (conversation.getMode() == Conversation.MODE_SINGLE) {
if (messageList.size() >= 1) {
makeFingerprintWarning(conversation.getLatestEncryption());
makeFingerprintWarning();
}
} else {
if (!conversation.getMucOptions().online()
@ -522,6 +613,40 @@ public class ConversationFragment extends Fragment {
}
}
private void decryptNext() {
Message next = this.mEncryptedMessages.peek();
PgpEngine engine = activity.xmppConnectionService.getPgpEngine();
if (next != null && engine != null && !mDecryptJobRunning) {
mDecryptJobRunning = true;
engine.decrypt(next, new UiCallback<Message>() {
@Override
public void userInputRequried(PendingIntent pi, Message message) {
mDecryptJobRunning = false;
askForPassphraseIntent = pi.getIntentSender();
showSnackbar(R.string.openpgp_messages_found,
R.string.decrypt, clickToDecryptListener);
}
@Override
public void success(Message message) {
mDecryptJobRunning = false;
mEncryptedMessages.remove();
activity.xmppConnectionService.updateMessage(message);
}
@Override
public void error(int error, Message message) {
message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED);
mDecryptJobRunning = false;
mEncryptedMessages.remove();
activity.xmppConnectionService.updateConversationUi();
}
});
}
}
private void messageSent() {
int size = this.messageList.size();
messagesView.setSelection(size - 1);
@ -594,14 +719,13 @@ public class ConversationFragment extends Fragment {
}
}
protected void makeFingerprintWarning(int latestEncryption) {
protected void makeFingerprintWarning() {
Set<String> knownFingerprints = conversation.getContact()
.getOtrFingerprints();
if ((latestEncryption == Message.ENCRYPTION_OTR)
&& (conversation.hasValidOtrSession()
if (conversation.hasValidOtrSession()
&& (!conversation.isMuted())
&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
.contains(conversation.getOtrFingerprint())))) {
.contains(conversation.getOtrFingerprint()))) {
showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
new OnClickListener() {

View file

@ -1,8 +1,6 @@
package eu.siacs.conversations.ui;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.os.Bundle;
import android.text.Editable;
@ -17,6 +15,7 @@ import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import eu.siacs.conversations.R;
@ -43,7 +42,7 @@ public class EditAccountActivity extends XmppActivity {
private TextView mServerInfoPep;
private TextView mSessionEst;
private TextView mOtrFingerprint;
private TextView mOtrFingerprintHeadline;
private RelativeLayout mOtrFingerprintBox;
private ImageButton mOtrFingerprintToClipboardButton;
private String jidToEdit;
@ -277,7 +276,7 @@ public class EditAccountActivity extends XmppActivity {
this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm);
this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint);
this.mOtrFingerprintHeadline = (TextView) findViewById(R.id.otr_fingerprint_headline);
this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box);
this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
this.mSaveButton = (Button) findViewById(R.id.save_button);
this.mCancelButton = (Button) findViewById(R.id.cancel_button);
@ -378,8 +377,7 @@ public class EditAccountActivity extends XmppActivity {
final String fingerprint = this.mAccount
.getOtrFingerprint(xmppConnectionService);
if (fingerprint != null) {
this.mOtrFingerprintHeadline.setVisibility(View.VISIBLE);
this.mOtrFingerprint.setVisibility(View.VISIBLE);
this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
this.mOtrFingerprint.setText(fingerprint);
this.mOtrFingerprintToClipboardButton
.setVisibility(View.VISIBLE);
@ -389,7 +387,7 @@ public class EditAccountActivity extends XmppActivity {
@Override
public void onClick(View v) {
if (OtrFingerprintToClipBoard(fingerprint)) {
if (copyTextToClipboard(fingerprint,R.string.otr_fingerprint)) {
Toast.makeText(
EditAccountActivity.this,
R.string.toast_message_otr_fingerprint,
@ -398,9 +396,7 @@ public class EditAccountActivity extends XmppActivity {
}
});
} else {
this.mOtrFingerprintToClipboardButton.setVisibility(View.GONE);
this.mOtrFingerprint.setVisibility(View.GONE);
this.mOtrFingerprintHeadline.setVisibility(View.GONE);
this.mOtrFingerprintBox.setVisibility(View.GONE);
}
} else {
if (this.mAccount.errorStatus()) {
@ -411,15 +407,4 @@ public class EditAccountActivity extends XmppActivity {
this.mStats.setVisibility(View.GONE);
}
}
private boolean OtrFingerprintToClipBoard(String fingerprint) {
ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
String label = getResources().getString(R.string.otr_fingerprint);
if (mClipBoardManager != null) {
ClipData mClipData = ClipData.newPlainText(label, fingerprint);
mClipBoardManager.setPrimaryClip(mClipData);
return true;
}
return false;
}
}

View file

@ -70,7 +70,8 @@ public class ManageAccountActivity extends XmppActivity {
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
ManageAccountActivity.this.getMenuInflater().inflate(R.menu.manageaccounts_context, menu);
ManageAccountActivity.this.getMenuInflater().inflate(
R.menu.manageaccounts_context, menu);
AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
this.selectedAccount = accountList.get(acmi.position);
if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) {
@ -122,6 +123,7 @@ public class ManageAccountActivity extends XmppActivity {
return true;
case R.id.mgmt_account_announce_pgp:
publishOpenPGPPublicKey(selectedAccount);
return true;
default:
return super.onContextItemSelected(item);
}
@ -187,7 +189,8 @@ public class ManageAccountActivity extends XmppActivity {
}
private void deleteAccount(final Account account) {
AlertDialog.Builder builder = new AlertDialog.Builder(ManageAccountActivity.this);
AlertDialog.Builder builder = new AlertDialog.Builder(
ManageAccountActivity.this);
builder.setTitle(getString(R.string.mgmt_account_are_you_sure));
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text));

View file

@ -158,8 +158,8 @@ public class PublishProfilePictureActivity extends XmppActivity {
if (this.avatarUri == null) {
if (this.account.getAvatar() != null
|| this.defaultUri == null) {
this.avatar.setImageBitmap(this.account.getImage(
getApplicationContext(), 384));
this.avatar.setImageBitmap(avatarService().get(account,
getPixel(194)));
if (this.defaultUri != null) {
this.avatar
.setOnLongClickListener(this.backToDefaultListener);

View file

@ -463,11 +463,11 @@ public class StartConversationActivity extends XmppActivity {
public boolean onCreateOptionsMenu(Menu menu) {
this.mOptionsMenu = menu;
getMenuInflater().inflate(R.menu.start_conversation, menu);
MenuItem menuCreateContact = (MenuItem) menu
MenuItem menuCreateContact = menu
.findItem(R.id.action_create_contact);
MenuItem menuCreateConference = (MenuItem) menu
MenuItem menuCreateConference = menu
.findItem(R.id.action_join_conference);
mMenuSearchView = (MenuItem) menu.findItem(R.id.action_search);
mMenuSearchView = menu.findItem(R.id.action_search);
mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
View mSearchView = mMenuSearchView.getActionView();
mSearchEditText = (EditText) mSearchView

View file

@ -12,6 +12,7 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
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.utils.ExceptionHelper;
@ -20,6 +21,8 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.AlertDialog.Builder;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
@ -55,7 +58,6 @@ public abstract class XmppActivity extends Activity {
public XmppConnectionService xmppConnectionService;
public boolean xmppConnectionServiceBound = false;
protected boolean handledViewIntent = false;
protected int mPrimaryTextColor;
protected int mSecondaryTextColor;
@ -400,8 +402,7 @@ public abstract class XmppActivity extends Activity {
private void quickEdit(final String previousValue,
final OnValueEdited callback, boolean password) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = (View) getLayoutInflater()
.inflate(R.layout.quickedit, null);
View view = getLayoutInflater().inflate(R.layout.quickedit, null);
final EditText editor = (EditText) view.findViewById(R.id.editor);
OnClickListener mClickListener = new OnClickListener() {
@ -448,7 +449,7 @@ public abstract class XmppActivity extends Activity {
listener.onPresenceSelected();
}
} else if (presences.size() == 1) {
String presence = (String) presences.asStringArray()[0];
String presence = presences.asStringArray()[0];
conversation.setNextPresence(presence);
listener.onPresenceSelected();
} else {
@ -526,6 +527,26 @@ public abstract class XmppActivity extends Activity {
return this.mSecondaryBackgroundColor;
}
public int getPixel(int dp) {
DisplayMetrics metrics = getResources().getDisplayMetrics();
return ((int) (dp * metrics.density));
}
public boolean copyTextToClipboard(String text,int labelResId) {
ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
String label = getResources().getString(labelResId);
if (mClipBoardManager != null) {
ClipData mClipData = ClipData.newPlainText(label, text);
mClipBoardManager.setPrimaryClip(mClipData);
return true;
}
return false;
}
public AvatarService avatarService() {
return xmppConnectionService.getAvatarService();
}
class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private Message message = null;

View file

@ -28,13 +28,14 @@ public class AccountAdapter extends ArrayAdapter<Account> {
if (view == null) {
LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = (View) inflater.inflate(R.layout.account_row, parent, false);
view = inflater.inflate(R.layout.account_row, parent, false);
}
TextView jid = (TextView) view.findViewById(R.id.account_jid);
jid.setText(account.getJid());
TextView statusView = (TextView) view.findViewById(R.id.account_status);
ImageView imageView = (ImageView) view.findViewById(R.id.account_image);
imageView.setImageBitmap(account.getImage(activity, 48));
imageView.setImageBitmap(activity.avatarService().get(account,
activity.getPixel(48)));
switch (account.getStatus()) {
case Account.STATUS_DISABLED:
statusView.setText(getContext().getString(

View file

@ -5,6 +5,7 @@ import java.util.List;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.XmppActivity;
@ -34,14 +35,14 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
if (view == null) {
LayoutInflater inflater = (LayoutInflater) activity
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = (View) inflater.inflate(R.layout.conversation_list_row,
view = inflater.inflate(R.layout.conversation_list_row,
parent, false);
}
Conversation conv = getItem(position);
Conversation conversation = getItem(position);
if (this.activity instanceof ConversationActivity) {
ConversationActivity activity = (ConversationActivity) this.activity;
if (!activity.isConversationsOverviewHideable()) {
if (conv == activity.getSelectedConversation()) {
if (conversation == activity.getSelectedConversation()) {
view.setBackgroundColor(activity
.getSecondaryBackgroundColor());
} else {
@ -53,65 +54,85 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
}
TextView convName = (TextView) view
.findViewById(R.id.conversation_name);
if (conv.getMode() == Conversation.MODE_SINGLE
if (conversation.getMode() == Conversation.MODE_SINGLE
|| activity.useSubjectToIdentifyConference()) {
convName.setText(conv.getName());
convName.setText(conversation.getName());
} else {
convName.setText(conv.getContactJid().split("/")[0]);
convName.setText(conversation.getContactJid().split("/")[0]);
}
TextView convLastMsg = (TextView) view
TextView mLastMessage = (TextView) view
.findViewById(R.id.conversation_lastmsg);
TextView mTimestamp = (TextView) view
.findViewById(R.id.conversation_lastupdate);
ImageView imagePreview = (ImageView) view
.findViewById(R.id.conversation_lastimage);
Message latestMessage = conv.getLatestMessage();
Message message = conversation.getLatestMessage();
if (latestMessage.getType() == Message.TYPE_TEXT
|| latestMessage.getType() == Message.TYPE_PRIVATE) {
if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP)
&& (latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
String body = Config.PARSE_EMOTICONS ? UIHelper
.transformAsciiEmoticons(latestMessage.getBody())
: latestMessage.getBody();
convLastMsg.setText(body);
} else {
convLastMsg.setText(R.string.encrypted_message_received);
}
convLastMsg.setVisibility(View.VISIBLE);
imagePreview.setVisibility(View.GONE);
} else if (latestMessage.getType() == Message.TYPE_IMAGE) {
if (latestMessage.getStatus() >= Message.STATUS_RECEIVED) {
convLastMsg.setVisibility(View.GONE);
imagePreview.setVisibility(View.VISIBLE);
activity.loadBitmap(latestMessage, imagePreview);
} else {
convLastMsg.setVisibility(View.VISIBLE);
imagePreview.setVisibility(View.GONE);
if (latestMessage.getStatus() == Message.STATUS_RECEIVED_OFFER) {
convLastMsg.setText(R.string.image_offered_for_download);
} else if (latestMessage.getStatus() == Message.STATUS_RECEIVING) {
convLastMsg.setText(R.string.receiving_image);
} else {
convLastMsg.setText("");
}
}
}
if (!conv.isRead()) {
if (!conversation.isRead()) {
convName.setTypeface(null, Typeface.BOLD);
convLastMsg.setTypeface(null, Typeface.BOLD);
} else {
convName.setTypeface(null, Typeface.NORMAL);
convLastMsg.setTypeface(null, Typeface.NORMAL);
}
((TextView) view.findViewById(R.id.conversation_lastupdate))
.setText(UIHelper.readableTimeDifference(getContext(), conv
.getLatestMessage().getTimeSent()));
if (message.getType() == Message.TYPE_IMAGE
|| message.getDownloadable() != null) {
Downloadable d = message.getDownloadable();
if (conversation.isRead()) {
mLastMessage.setTypeface(null, Typeface.ITALIC);
} else {
mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC);
}
if (d != null) {
mLastMessage.setVisibility(View.VISIBLE);
imagePreview.setVisibility(View.GONE);
if (d.getStatus() == Downloadable.STATUS_CHECKING) {
mLastMessage.setText(R.string.checking_image);
} else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
mLastMessage.setText(R.string.receiving_image);
} else if (d.getStatus() == Downloadable.STATUS_OFFER) {
mLastMessage.setText(R.string.image_offered_for_download);
} else if (d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
mLastMessage.setText(R.string.image_offered_for_download);
} else if (d.getStatus() == Downloadable.STATUS_DELETED) {
mLastMessage.setText(R.string.image_file_deleted);
} else {
mLastMessage.setText("");
}
} else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
imagePreview.setVisibility(View.GONE);
mLastMessage.setVisibility(View.VISIBLE);
mLastMessage.setText(R.string.encrypted_message_received);
} else {
mLastMessage.setVisibility(View.GONE);
imagePreview.setVisibility(View.VISIBLE);
activity.loadBitmap(message, imagePreview);
}
} else {
if ((message.getEncryption() != Message.ENCRYPTION_PGP)
&& (message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) {
String body = Config.PARSE_EMOTICONS ? UIHelper
.transformAsciiEmoticons(message.getBody()) : message
.getBody();
mLastMessage.setText(body);
} else {
mLastMessage.setText(R.string.encrypted_message_received);
}
if (!conversation.isRead()) {
mLastMessage.setTypeface(null, Typeface.BOLD);
} else {
mLastMessage.setTypeface(null, Typeface.NORMAL);
}
mLastMessage.setVisibility(View.VISIBLE);
imagePreview.setVisibility(View.GONE);
}
mTimestamp.setText(UIHelper.readableTimeDifference(getContext(),
conversation.getLatestMessage().getTimeSent()));
ImageView profilePicture = (ImageView) view
.findViewById(R.id.conversation_image);
profilePicture.setImageBitmap(conv.getImage(activity, 56));
profilePicture.setImageBitmap(activity.avatarService().get(
conversation, activity.getPixel(56)));
return view;
}

View file

@ -47,11 +47,11 @@ public class KnownHostsAdapter extends ArrayAdapter<String> {
@Override
protected void publishResults(CharSequence constraint,
FilterResults results) {
ArrayList<String> filteredList = (ArrayList<String>) results.values;
ArrayList filteredList = (ArrayList) results.values;
if (results != null && results.count > 0) {
clear();
for (String c : filteredList) {
add(c);
for (Object c : filteredList) {
add((String) c);
}
notifyDataSetChanged();
}

View file

@ -4,6 +4,7 @@ import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.ListItem;
import eu.siacs.conversations.ui.XmppActivity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
@ -14,8 +15,11 @@ import android.widget.TextView;
public class ListItemAdapter extends ArrayAdapter<ListItem> {
public ListItemAdapter(Context context, List<ListItem> objects) {
super(context, 0, objects);
protected XmppActivity activity;
public ListItemAdapter(XmppActivity activity, List<ListItem> objects) {
super(activity, 0, objects);
this.activity = activity;
}
@Override
@ -24,7 +28,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
ListItem item = getItem(position);
if (view == null) {
view = (View) inflater.inflate(R.layout.contact, parent, false);
view = inflater.inflate(R.layout.contact, parent, false);
}
TextView name = (TextView) view.findViewById(R.id.contact_display_name);
TextView jid = (TextView) view.findViewById(R.id.contact_jid);
@ -32,7 +36,8 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> {
jid.setText(item.getJid());
name.setText(item.getDisplayName());
picture.setImageBitmap(item.getImage(48, getContext()));
picture.setImageBitmap(activity.avatarService().get(item,
activity.getPixel(48)));
return view;
}

View file

@ -1,6 +1,5 @@
package eu.siacs.conversations.ui.adapter;
import java.util.HashMap;
import java.util.List;
import eu.siacs.conversations.Config;
@ -9,11 +8,10 @@ import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Message.ImageParams;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.utils.UIHelper;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.text.Spannable;
import android.text.SpannableString;
@ -40,31 +38,26 @@ public class MessageAdapter extends ArrayAdapter<Message> {
private ConversationActivity activity;
private Bitmap accountBitmap;
private BitmapCache mBitmapCache = new BitmapCache();
private DisplayMetrics metrics;
private OnContactPictureClicked mOnContactPictureClickedListener;
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
private OnLongClickListener openContextMenu = new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
v.showContextMenu();
return true;
}
};
public MessageAdapter(ConversationActivity activity, List<Message> messages) {
super(activity, 0, messages);
this.activity = activity;
metrics = getContext().getResources().getDisplayMetrics();
}
private Bitmap getSelfBitmap() {
if (this.accountBitmap == null) {
if (getCount() > 0) {
this.accountBitmap = getItem(0).getConversation().getAccount()
.getImage(getContext(), 48);
}
}
return this.accountBitmap;
}
public void setOnContactPictureClicked(OnContactPictureClicked listener) {
this.mOnContactPictureClickedListener = listener;
}
@ -101,13 +94,14 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI
&& message.getMergedStatus() <= Message.STATUS_RECEIVED;
if (message.getType() == Message.TYPE_IMAGE) {
String[] fileParams = message.getBody().split(",");
try {
long size = Long.parseLong(fileParams[0]);
filesize = size / 1024 + " KB";
} catch (NumberFormatException e) {
filesize = "0 KB";
if (message.getType() == Message.TYPE_IMAGE
|| message.getDownloadable() != null) {
ImageParams params = message.getImageParams();
if (params.size != 0) {
filesize = params.size / 1024 + " KB";
}
if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) {
error = true;
}
}
switch (message.getMergedStatus()) {
@ -134,14 +128,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
info = getContext().getString(R.string.send_failed);
error = true;
break;
case Message.STATUS_SEND_REJECTED:
info = getContext().getString(R.string.send_rejected);
error = true;
break;
case Message.STATUS_RECEPTION_FAILED:
info = getContext().getString(R.string.reception_failed);
error = true;
break;
default:
if (multiReceived) {
Contact contact = message.getContact();
@ -268,6 +254,22 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder.messageBody.setTextIsSelectable(true);
}
private void displayDownloadableMessage(ViewHolder viewHolder,
final Message message, int resid) {
viewHolder.image.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.GONE);
viewHolder.download_button.setVisibility(View.VISIBLE);
viewHolder.download_button.setText(resid);
viewHolder.download_button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
startDonwloadable(message);
}
});
viewHolder.download_button.setOnLongClickListener(openContextMenu);
}
private void displayImageMessage(ViewHolder viewHolder,
final Message message) {
if (viewHolder.download_button != null) {
@ -275,23 +277,19 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
viewHolder.messageBody.setVisibility(View.GONE);
viewHolder.image.setVisibility(View.VISIBLE);
String[] fileParams = message.getBody().split(",");
if (fileParams.length == 3) {
double target = metrics.density * 288;
int w = Integer.parseInt(fileParams[1]);
int h = Integer.parseInt(fileParams[2]);
int scalledW;
int scalledH;
if (w <= h) {
scalledW = (int) (w / ((double) h / target));
scalledH = (int) target;
} else {
scalledW = (int) target;
scalledH = (int) (h / ((double) w / target));
}
viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
scalledW, scalledH));
ImageParams params = message.getImageParams();
double target = metrics.density * 288;
int scalledW;
int scalledH;
if (params.width <= params.height) {
scalledW = (int) (params.width / ((double) params.height / target));
scalledH = (int) target;
} else {
scalledW = (int) target;
scalledH = (int) (params.height / ((double) params.width / target));
}
viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams(
scalledW, scalledH));
activity.loadBitmap(message, viewHolder.image);
viewHolder.image.setOnClickListener(new OnClickListener() {
@ -303,23 +301,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
getContext().startActivity(intent);
}
});
viewHolder.image.setOnLongClickListener(new OnLongClickListener() {
@Override
public boolean onLongClick(View v) {
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM,
activity.xmppConnectionService.getFileBackend()
.getJingleFileUri(message));
shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
shareIntent.setType("image/webp");
getContext().startActivity(
Intent.createChooser(shareIntent,
getContext().getText(R.string.share_with)));
return true;
}
});
viewHolder.image.setOnLongClickListener(openContextMenu);
}
@Override
@ -331,17 +313,22 @@ public class MessageAdapter extends ArrayAdapter<Message> {
viewHolder = new ViewHolder();
switch (type) {
case NULL:
view = (View) activity.getLayoutInflater().inflate(
view = activity.getLayoutInflater().inflate(
R.layout.message_null, parent, false);
break;
case SENT:
view = (View) activity.getLayoutInflater().inflate(
view = activity.getLayoutInflater().inflate(
R.layout.message_sent, parent, false);
viewHolder.message_box = (LinearLayout) view
.findViewById(R.id.message_box);
viewHolder.contact_picture = (ImageView) view
.findViewById(R.id.message_photo);
viewHolder.contact_picture.setImageBitmap(getSelfBitmap());
viewHolder.contact_picture.setImageBitmap(activity
.avatarService().get(
item.getConversation().getAccount(),
activity.getPixel(48)));
viewHolder.download_button = (Button) view
.findViewById(R.id.download_button);
viewHolder.indicator = (ImageView) view
.findViewById(R.id.security_indicator);
viewHolder.image = (ImageView) view
@ -355,21 +342,18 @@ public class MessageAdapter extends ArrayAdapter<Message> {
view.setTag(viewHolder);
break;
case RECEIVED:
view = (View) activity.getLayoutInflater().inflate(
view = activity.getLayoutInflater().inflate(
R.layout.message_received, parent, false);
viewHolder.message_box = (LinearLayout) view
.findViewById(R.id.message_box);
viewHolder.contact_picture = (ImageView) view
.findViewById(R.id.message_photo);
viewHolder.download_button = (Button) view
.findViewById(R.id.download_button);
if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
viewHolder.contact_picture.setImageBitmap(mBitmapCache.get(
item.getConversation().getContact(), getContext()));
viewHolder.contact_picture.setImageBitmap(activity
.avatarService().get(item.getContact(),
activity.getPixel(48)));
}
viewHolder.indicator = (ImageView) view
.findViewById(R.id.security_indicator);
@ -382,15 +366,17 @@ public class MessageAdapter extends ArrayAdapter<Message> {
view.setTag(viewHolder);
break;
case STATUS:
view = (View) activity.getLayoutInflater().inflate(
view = activity.getLayoutInflater().inflate(
R.layout.message_status, parent, false);
viewHolder.contact_picture = (ImageView) view
.findViewById(R.id.message_photo);
if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
viewHolder.contact_picture.setImageBitmap(mBitmapCache.get(
item.getConversation().getContact(), getContext()));
viewHolder.contact_picture.setAlpha(128);
viewHolder.contact_picture.setImageBitmap(activity
.avatarService().get(
item.getConversation().getContact(),
activity.getPixel(32)));
viewHolder.contact_picture.setAlpha(0.5f);
viewHolder.contact_picture
.setOnClickListener(new OnClickListener() {
@ -465,38 +451,40 @@ public class MessageAdapter extends ArrayAdapter<Message> {
if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
Contact contact = item.getContact();
if (contact != null) {
viewHolder.contact_picture.setImageBitmap(mBitmapCache.get(
contact, getContext()));
viewHolder.contact_picture.setImageBitmap(activity
.avatarService()
.get(contact, activity.getPixel(48)));
} else {
String name = item.getPresence();
if (name == null) {
name = item.getCounterpart();
}
viewHolder.contact_picture.setImageBitmap(mBitmapCache.get(
name, getContext()));
viewHolder.contact_picture.setImageBitmap(activity
.avatarService().get(name, activity.getPixel(48)));
}
}
}
if (item.getType() == Message.TYPE_IMAGE) {
if (item.getStatus() == Message.STATUS_RECEIVING) {
if (item.getType() == Message.TYPE_IMAGE
|| item.getDownloadable() != null) {
Downloadable d = item.getDownloadable();
if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) {
displayInfoMessage(viewHolder, R.string.receiving_image);
} else if (item.getStatus() == Message.STATUS_RECEIVED_OFFER) {
viewHolder.image.setVisibility(View.GONE);
viewHolder.messageBody.setVisibility(View.GONE);
viewHolder.download_button.setVisibility(View.VISIBLE);
viewHolder.download_button
.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
Downloadable downloadable = item
.getDownloadable();
if (downloadable != null) {
downloadable.start();
}
}
});
} else if (d != null
&& d.getStatus() == Downloadable.STATUS_CHECKING) {
displayInfoMessage(viewHolder, R.string.checking_image);
} else if (d != null
&& d.getStatus() == Downloadable.STATUS_DELETED) {
displayInfoMessage(viewHolder, R.string.image_file_deleted);
} else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) {
displayDownloadableMessage(viewHolder, item,
R.string.download_image);
} else if (d != null
&& d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) {
displayDownloadableMessage(viewHolder, item,
R.string.check_image_filesize);
} else if (d != null && d.getStatus() == Downloadable.STATUS_FAILED) {
displayInfoMessage(viewHolder, R.string.image_transmission_failed);
} else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED)
|| (item.getEncryption() == Message.ENCRYPTION_NONE)
|| (item.getEncryption() == Message.ENCRYPTION_OTR)) {
@ -534,6 +522,16 @@ public class MessageAdapter extends ArrayAdapter<Message> {
return view;
}
public void startDonwloadable(Message message) {
Downloadable downloadable = message.getDownloadable();
if (downloadable != null) {
if (!downloadable.start()) {
Toast.makeText(activity, R.string.not_connected_try_again,
Toast.LENGTH_SHORT).show();
}
}
}
private static class ViewHolder {
protected LinearLayout message_box;
@ -547,30 +545,6 @@ public class MessageAdapter extends ArrayAdapter<Message> {
}
private class BitmapCache {
private HashMap<String, Bitmap> contactBitmaps = new HashMap<String, Bitmap>();
private HashMap<String, Bitmap> unknownBitmaps = new HashMap<String, Bitmap>();
public Bitmap get(Contact contact, Context context) {
if (!contactBitmaps.containsKey(contact.getJid())) {
contactBitmaps.put(contact.getJid(),
contact.getImage(48, context));
}
return contactBitmaps.get(contact.getJid());
}
public Bitmap get(String name, Context context) {
if (unknownBitmaps.containsKey(name)) {
return unknownBitmaps.get(name);
} else {
Bitmap bm = UIHelper
.getContactPicture(name, 48, context, false);
unknownBitmaps.put(name, bm);
return bm;
}
}
}
public interface OnContactPictureClicked {
public void onContactPictureClicked(Message message);
}

View file

@ -5,7 +5,6 @@ import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import eu.siacs.conversations.entities.Account;
import android.util.Base64;
@ -28,9 +27,11 @@ public class CryptoHelper {
}
public static byte[] hexToBytes(String hexString) {
byte[] array = new BigInteger(hexString, 16).toByteArray();
if (array[0] == 0) {
array = Arrays.copyOfRange(array, 1, array.length);
int len = hexString.length();
byte[] array = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character
.digit(hexString.charAt(i + 1), 16));
}
return array;
}

View file

@ -33,7 +33,7 @@ public class DNSHelper {
for (String dnsserver : dns) {
InetAddress ip = InetAddress.getByName(dnsserver);
Bundle b = queryDNS(host, ip);
if (b.containsKey("name")) {
if (b.containsKey("values")) {
return b;
} else if (b.containsKey("error")
&& "nosrv".equals(b.getString("error", null))) {
@ -45,7 +45,7 @@ public class DNSHelper {
}
public static Bundle queryDNS(String host, InetAddress dnsServer) {
Bundle namePort = new Bundle();
Bundle bundle = new Bundle();
try {
String qname = "_xmpp-client._tcp." + host;
Log.d(Config.LOGTAG,
@ -133,42 +133,28 @@ public class DNSHelper {
}
if (result.size() == 0) {
namePort.putString("error", "nosrv");
return namePort;
bundle.putString("error", "nosrv");
return bundle;
}
// we now have a list of servers to try :-)
// classic name/port pair
String resultName = result.get(0).getName();
namePort.putString("name", resultName);
namePort.putInt("port", result.get(0).getPort());
if (ips4.containsKey(resultName)) {
// we have an ip!
ArrayList<String> ip = ips4.get(resultName);
Collections.shuffle(ip, rnd);
namePort.putString("ipv4", ip.get(0));
}
if (ips6.containsKey(resultName)) {
ArrayList<String> ip = ips6.get(resultName);
Collections.shuffle(ip, rnd);
namePort.putString("ipv6", ip.get(0));
}
// add all other records
int i = 0;
ArrayList<Bundle> values = new ArrayList<Bundle>();
for (SRV srv : result) {
namePort.putString("name" + i, srv.getName());
namePort.putInt("port" + i, srv.getPort());
i++;
Bundle namePort = new Bundle();
namePort.putString("name", srv.getName());
namePort.putInt("port", srv.getPort());
if (ips4.containsKey(srv.getName())) {
ArrayList<String> ip = ips4.get(srv.getName());
Collections.shuffle(ip, rnd);
namePort.putString("ipv4", ip.get(0));
}
values.add(namePort);
}
bundle.putParcelableArrayList("values", values);
} catch (SocketTimeoutException e) {
namePort.putString("error", "timeout");
bundle.putString("error", "timeout");
} catch (Exception e) {
namePort.putString("error", "unhandled");
bundle.putString("error", "unhandled");
}
return namePort;
return bundle;
}
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();

View file

@ -0,0 +1,161 @@
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.siacs.conversations.utils;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
public class ExifHelper {
private static final String TAG = "CameraExif";
public static int getOrientation(InputStream is) {
if (is == null) {
return 0;
}
byte[] buf = new byte[8];
int length = 0;
// ISO/IEC 10918-1:1993(E)
while (read(is, buf, 2) && (buf[0] & 0xFF) == 0xFF) {
int marker = buf[1] & 0xFF;
// Check if the marker is a padding.
if (marker == 0xFF) {
continue;
}
// Check if the marker is SOI or TEM.
if (marker == 0xD8 || marker == 0x01) {
continue;
}
// Check if the marker is EOI or SOS.
if (marker == 0xD9 || marker == 0xDA) {
return 0;
}
// Get the length and check if it is reasonable.
if (!read(is, buf, 2)) {
return 0;
}
length = pack(buf, 0, 2, false);
if (length < 2) {
Log.e(TAG, "Invalid length");
return 0;
}
length -= 2;
// Break if the marker is EXIF in APP1.
if (marker == 0xE1 && length >= 6) {
if (!read(is, buf, 6)) return 0;
length -= 6;
if (pack(buf, 0, 4, false) == 0x45786966 &&
pack(buf, 4, 2, false) == 0) {
break;
}
}
// Skip other markers.
try {
is.skip(length);
} catch (IOException ex) {
return 0;
}
length = 0;
}
// JEITA CP-3451 Exif Version 2.2
if (length > 8) {
int offset = 0;
byte[] jpeg = new byte[length];
if (!read(is, jpeg, length)) {
return 0;
}
// Identify the byte order.
int tag = pack(jpeg, offset, 4, false);
if (tag != 0x49492A00 && tag != 0x4D4D002A) {
Log.e(TAG, "Invalid byte order");
return 0;
}
boolean littleEndian = (tag == 0x49492A00);
// Get the offset and check if it is reasonable.
int count = pack(jpeg, offset + 4, 4, littleEndian) + 2;
if (count < 10 || count > length) {
Log.e(TAG, "Invalid offset");
return 0;
}
offset += count;
length -= count;
// Get the count and go through all the elements.
count = pack(jpeg, offset - 2, 2, littleEndian);
while (count-- > 0 && length >= 12) {
// Get the tag and check if it is orientation.
tag = pack(jpeg, offset, 2, littleEndian);
if (tag == 0x0112) {
// We do not really care about type and count, do we?
int orientation = pack(jpeg, offset + 8, 2, littleEndian);
switch (orientation) {
case 1:
return 0;
case 3:
return 180;
case 6:
return 90;
case 8:
return 270;
}
Log.i(TAG, "Unsupported orientation");
return 0;
}
offset += 12;
length -= 12;
}
}
Log.i(TAG, "Orientation not found");
return 0;
}
private static int pack(byte[] bytes, int offset, int length,
boolean littleEndian) {
int step = 1;
if (littleEndian) {
offset += length - 1;
step = -1;
}
int value = 0;
while (length-- > 0) {
value = (value << 8) | (bytes[offset] & 0xFF);
offset += step;
}
return value;
}
private static boolean read(InputStream is, byte[] buf, int length) {
try {
return is.read(buf, 0, length) == length;
} catch (IOException ex) {
return false;
}
}
}

View file

@ -22,7 +22,7 @@ public class PhoneHelper {
final String[] PROJECTION = new String[] { ContactsContract.Data._ID,
ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.PHOTO_THUMBNAIL_URI,
ContactsContract.Data.PHOTO_URI,
ContactsContract.Data.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Im.DATA };
@ -50,10 +50,8 @@ public class PhoneHelper {
"displayname",
cursor.getString(cursor
.getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
contact.putString(
"photouri",
cursor.getString(cursor
.getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI)));
contact.putString("photouri", cursor.getString(cursor
.getColumnIndex(ContactsContract.Data.PHOTO_URI)));
contact.putString("lookup", cursor.getString(cursor
.getColumnIndex(ContactsContract.Data.LOOKUP_KEY)));

View file

@ -1,22 +1,18 @@
package eu.siacs.conversations.utils;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.ManageAccountActivity;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Notification;
import android.app.NotificationManager;
@ -25,28 +21,15 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.net.Uri;
import android.provider.ContactsContract.Contacts;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.text.format.DateFormat;
import android.text.format.DateUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.QuickContactBadge;
import android.widget.TextView;
public class UIHelper {
private static final int BG_COLOR = 0xFF181818;
private static final int FG_COLOR = 0xFFFAFAFA;
private static final int TRANSPARENT = 0x00000000;
private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME
@ -123,167 +106,6 @@ public class UIHelper {
}
}
public static int getRealPx(int dp, Context context) {
final DisplayMetrics metrics = context.getResources()
.getDisplayMetrics();
return ((int) (dp * metrics.density));
}
private static int getNameColor(String name) {
/*
* int holoColors[] = { 0xFF1da9da, 0xFFb368d9, 0xFF83b600, 0xFFffa713,
* 0xFFe92727 };
*/
int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5,
0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722,
0xFF795548, 0xFF607d8b };
return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)];
}
private static void drawTile(Canvas canvas, String letter, int tileColor,
int textColor, int left, int top, int right, int bottom) {
Paint tilePaint = new Paint(), textPaint = new Paint();
tilePaint.setColor(tileColor);
textPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(textColor);
textPaint.setTypeface(Typeface.create("sans-serif-light",
Typeface.NORMAL));
textPaint.setTextSize((float) ((right - left) * 0.8));
Rect rect = new Rect();
canvas.drawRect(new Rect(left, top, right, bottom), tilePaint);
textPaint.getTextBounds(letter, 0, 1, rect);
float width = textPaint.measureText(letter);
canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom)
/ 2 + rect.height() / 2, textPaint);
}
private static Bitmap getUnknownContactPicture(String[] names, int size,
int bgColor, int fgColor) {
int tiles = (names.length > 4) ? 4 : (names.length < 1) ? 1
: names.length;
Bitmap bitmap = Bitmap
.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
String[] letters = new String[tiles];
int[] colors = new int[tiles];
if (names.length < 1) {
letters[0] = "?";
colors[0] = 0xFFe92727;
} else {
for (int i = 0; i < tiles; ++i) {
letters[i] = (names[i].length() > 0) ? names[i].substring(0, 1)
.toUpperCase(Locale.US) : " ";
colors[i] = getNameColor(names[i]);
}
if (names.length > 4) {
letters[3] = "\u2026"; // Unicode ellipsis
colors[3] = 0xFF202020;
}
}
bitmap.eraseColor(bgColor);
switch (tiles) {
case 1:
drawTile(canvas, letters[0], colors[0], fgColor, 0, 0, size, size);
break;
case 2:
drawTile(canvas, letters[0], colors[0], fgColor, 0, 0,
size / 2 - 1, size);
drawTile(canvas, letters[1], colors[1], fgColor, size / 2 + 1, 0,
size, size);
break;
case 3:
drawTile(canvas, letters[0], colors[0], fgColor, 0, 0,
size / 2 - 1, size);
drawTile(canvas, letters[1], colors[1], fgColor, size / 2 + 1, 0,
size, size / 2 - 1);
drawTile(canvas, letters[2], colors[2], fgColor, size / 2 + 1,
size / 2 + 1, size, size);
break;
case 4:
drawTile(canvas, letters[0], colors[0], fgColor, 0, 0,
size / 2 - 1, size / 2 - 1);
drawTile(canvas, letters[1], colors[1], fgColor, 0, size / 2 + 1,
size / 2 - 1, size);
drawTile(canvas, letters[2], colors[2], fgColor, size / 2 + 1, 0,
size, size / 2 - 1);
drawTile(canvas, letters[3], colors[3], fgColor, size / 2 + 1,
size / 2 + 1, size, size);
break;
}
return bitmap;
}
private static Bitmap getMucContactPicture(Conversation conversation,
int size, int bgColor, int fgColor) {
List<User> members = conversation.getMucOptions().getUsers();
if (members.size() == 0) {
return getUnknownContactPicture(
new String[] { conversation.getName() }, size, bgColor,
fgColor);
}
ArrayList<String> names = new ArrayList<String>();
names.add(conversation.getMucOptions().getActualNick());
for (User user : members) {
names.add(user.getName());
if (names.size() > 4) {
break;
}
}
String[] mArrayNames = new String[names.size()];
names.toArray(mArrayNames);
return getUnknownContactPicture(mArrayNames, size, bgColor, fgColor);
}
public static Bitmap getContactPicture(Conversation conversation,
int dpSize, Context context, boolean notification) {
if (conversation.getMode() == Conversation.MODE_SINGLE) {
return getContactPicture(conversation.getContact(), dpSize,
context, notification);
} else {
int fgColor = UIHelper.FG_COLOR, bgColor = (notification) ? UIHelper.BG_COLOR
: UIHelper.TRANSPARENT;
return getMucContactPicture(conversation,
getRealPx(dpSize, context), bgColor, fgColor);
}
}
public static Bitmap getContactPicture(Contact contact, int dpSize,
Context context, boolean notification) {
String uri = contact.getProfilePhoto();
if (uri == null) {
return getContactPicture(contact.getDisplayName(), dpSize, context,
notification);
}
try {
Bitmap bm = BitmapFactory.decodeStream(context.getContentResolver()
.openInputStream(Uri.parse(uri)));
return Bitmap.createScaledBitmap(bm, getRealPx(dpSize, context),
getRealPx(dpSize, context), false);
} catch (FileNotFoundException e) {
return getContactPicture(contact.getDisplayName(), dpSize, context,
notification);
}
}
public static Bitmap getContactPicture(String name, int dpSize,
Context context, boolean notification) {
int fgColor = UIHelper.FG_COLOR, bgColor = (notification) ? UIHelper.BG_COLOR
: UIHelper.TRANSPARENT;
return getUnknownContactPicture(new String[] { name },
getRealPx(dpSize, context), bgColor, fgColor);
}
public static void showErrorNotification(Context context,
List<Account> accounts) {
NotificationManager mNotificationManager = (NotificationManager) context
@ -326,16 +148,6 @@ public class UIHelper {
mNotificationManager.notify(1111, notification);
}
public static void prepareContactBadge(final Activity activity,
QuickContactBadge badge, final Contact contact, Context context) {
if (contact.getSystemAccount() != null) {
String[] systemAccount = contact.getSystemAccount().split("#");
long id = Long.parseLong(systemAccount[0]);
badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1]));
}
badge.setImageBitmap(contact.getImage(72, context));
}
@SuppressLint("InflateParams")
public static AlertDialog getVerifyFingerprintDialog(
final ConversationActivity activity,
@ -370,25 +182,6 @@ public class UIHelper {
return builder.create();
}
public static Bitmap getSelfContactPicture(Account account, int size,
boolean showPhoneSelfContactPicture, Context context) {
if (showPhoneSelfContactPicture) {
Uri selfiUri = PhoneHelper.getSefliUri(context);
if (selfiUri != null) {
try {
return BitmapFactory.decodeStream(context
.getContentResolver().openInputStream(selfiUri));
} catch (FileNotFoundException e) {
return getContactPicture(account.getJid(), size, context,
false);
}
}
return getContactPicture(account.getJid(), size, context, false);
} else {
return getContactPicture(account.getJid(), size, context, false);
}
}
private final static class EmoticonPattern {
Pattern pattern;
String replacement;

View file

@ -4,14 +4,17 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.Map.Entry;
@ -22,14 +25,19 @@ import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;
import org.apache.http.conn.ssl.StrictHostnameVerifier;
import org.xmlpull.v1.XmlPullParserException;
import de.duenndns.ssl.MemorizingTrustManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.SystemClock;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.SparseArray;
import eu.siacs.conversations.Config;
@ -80,6 +88,7 @@ public class XmppConnection implements Runnable {
private SparseArray<String> messageReceipts = new SparseArray<String>();
private boolean usingCompression = false;
private boolean usingEncryption = false;
private int stanzasReceived = 0;
private int stanzasSent = 0;
@ -104,6 +113,7 @@ public class XmppConnection implements Runnable {
private OnBindListener bindListener = null;
private OnMessageAcknowledged acknowledgedListener = null;
private MemorizingTrustManager mMemorizingTrustManager;
private final Context applicationContext;
public XmppConnection(Account account, XmppConnectionService service) {
this.mRandom = service.getRNG();
@ -112,6 +122,7 @@ public class XmppConnection implements Runnable {
this.wakeLock = service.getPowerManager().newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, account.getJid());
tagWriter = new TagWriter();
applicationContext = service.getApplicationContext();
}
protected void changeStatus(int nextStatus) {
@ -135,6 +146,7 @@ public class XmppConnection implements Runnable {
protected void connect() {
Log.d(Config.LOGTAG, account.getJid() + ": connecting");
usingCompression = false;
usingEncryption = false;
lastConnect = SystemClock.elapsedRealtime();
lastPingSent = SystemClock.elapsedRealtime();
this.attempt++;
@ -145,29 +157,47 @@ public class XmppConnection implements Runnable {
tagWriter = new TagWriter();
packetCallbacks.clear();
this.changeStatus(Account.STATUS_CONNECTING);
Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
if ("timeout".equals(namePort.getString("error"))) {
Bundle result = DNSHelper.getSRVRecord(account.getServer());
ArrayList<Parcelable> values = result.getParcelableArrayList("values");
if ("timeout".equals(result.getString("error"))) {
Log.d(Config.LOGTAG, account.getJid() + ": dns timeout");
this.changeStatus(Account.STATUS_OFFLINE);
return;
}
String srvRecordServer = namePort.getString("name");
String srvIpServer = namePort.getString("ipv4");
int srvRecordPort = namePort.getInt("port");
if (srvRecordServer != null) {
if (srvIpServer != null) {
Log.d(Config.LOGTAG, account.getJid()
+ ": using values from dns " + srvRecordServer
+ "[" + srvIpServer + "]:" + srvRecordPort);
socket = new Socket(srvIpServer, srvRecordPort);
} else {
Log.d(Config.LOGTAG, account.getJid()
+ ": using values from dns " + srvRecordServer
+ ":" + srvRecordPort);
socket = new Socket(srvRecordServer, srvRecordPort);
}
} else if (namePort.containsKey("error")
&& "nosrv".equals(namePort.getString("error", null))) {
} else if (values != null) {
int i = 0;
boolean socketError = true;
while (socketError && values.size() > i) {
Bundle namePort = (Bundle) values.get(i);
try {
String srvRecordServer = namePort.getString("name");
int srvRecordPort = namePort.getInt("port");
String srvIpServer = namePort.getString("ipv4");
InetSocketAddress addr;
if (srvIpServer!=null) {
addr = new InetSocketAddress(srvIpServer, srvRecordPort);
Log.d(Config.LOGTAG, account.getJid()
+ ": using values from dns " + srvRecordServer
+ "[" + srvIpServer + "]:" + srvRecordPort);
} else {
addr = new InetSocketAddress(srvRecordServer, srvRecordPort);
Log.d(Config.LOGTAG, account.getJid()
+ ": using values from dns "
+ srvRecordServer + ":" + srvRecordPort);
}
socket = new Socket();
socket.connect(addr, 20000);
socketError = false;
} catch (UnknownHostException e) {
i++;
} catch (IOException e) {
i++;
}
}
if (socketError) {
throw new IOException();
}
} else if (result.containsKey("error")
&& "nosrv".equals(result.getString("error", null))) {
socket = new Socket(account.getServer(), 5222);
} else {
Log.d(Config.LOGTAG, account.getJid()
@ -504,6 +534,15 @@ 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(Tag currentTag) throws XmlPullParserException,
IOException {
tagReader.readTag();
@ -515,11 +554,27 @@ public class XmppConnection implements Runnable {
SSLSocketFactory factory = sc.getSocketFactory();
HostnameVerifier verifier = this.mMemorizingTrustManager
.wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier());
.wrapHostnameVerifier(new StrictHostnameVerifier());
SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,
socket.getInetAddress().getHostAddress(), socket.getPort(),
true);
// Support all protocols except legacy SSL.
// The min SDK version prevents us having to worry about SSLv2. In
// future, this may be
// true of SSLv3 as well.
final String[] supportProtocols;
if (enableLegacySSL()) {
supportProtocols = sslSocket.getSupportedProtocols();
} else {
final List<String> supportedProtocols = new LinkedList<String>(
Arrays.asList(sslSocket.getSupportedProtocols()));
supportedProtocols.remove("SSLv3");
supportProtocols = new String[supportedProtocols.size()];
supportedProtocols.toArray(supportProtocols);
}
sslSocket.setEnabledProtocols(supportProtocols);
if (verifier != null
&& !verifier.verify(account.getServer(),
sslSocket.getSession())) {
@ -533,6 +588,7 @@ public class XmppConnection implements Runnable {
sendStartStream();
Log.d(Config.LOGTAG, account.getJid()
+ ": TLS connection established");
usingEncryption = true;
processStream(tagReader.readTag());
sslSocket.close();
} catch (NoSuchAlgorithmException e1) {
@ -562,20 +618,20 @@ public class XmppConnection implements Runnable {
private void processStreamFeatures(Tag currentTag)
throws XmlPullParserException, IOException {
this.streamFeatures = tagReader.readElement(currentTag);
if (this.streamFeatures.hasChild("starttls")
&& account.isOptionSet(Account.OPTION_USETLS)) {
if (this.streamFeatures.hasChild("starttls") && !usingEncryption) {
sendStartTLS();
} else if (compressionAvailable()) {
sendCompressionZlib();
} else if (this.streamFeatures.hasChild("register")
&& (account.isOptionSet(Account.OPTION_REGISTER))) {
&& account.isOptionSet(Account.OPTION_REGISTER)
&& usingEncryption) {
sendRegistryRequest();
} else if (!this.streamFeatures.hasChild("register")
&& (account.isOptionSet(Account.OPTION_REGISTER))) {
&& account.isOptionSet(Account.OPTION_REGISTER)) {
changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED);
disconnect(true);
} else if (this.streamFeatures.hasChild("mechanisms")
&& shouldAuthenticate) {
&& shouldAuthenticate && usingEncryption) {
List<String> mechanisms = extractMechanisms(streamFeatures
.findChild("mechanisms"));
if (mechanisms.contains("PLAIN")) {
@ -591,6 +647,10 @@ public class XmppConnection implements Runnable {
this.tagWriter.writeStanzaAsync(resume);
} else if (this.streamFeatures.hasChild("bind") && shouldBind) {
sendBindRequest();
} else {
Log.d(Config.LOGTAG, account.getJid()
+ ": incompatible server. disconnecting");
disconnect(true);
}
}
@ -910,7 +970,7 @@ public class XmppConnection implements Runnable {
}
public void disconnect(boolean force) {
Log.d(Config.LOGTAG, account.getJid()+": disconnecting");
Log.d(Config.LOGTAG, account.getJid() + ": disconnecting");
try {
if (force) {
socket.close();

View file

@ -16,6 +16,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
@ -33,17 +34,18 @@ public class JingleConnection implements Downloadable {
private JingleConnectionManager mJingleConnectionManager;
private XmppConnectionService mXmppConnectionService;
public static final int STATUS_INITIATED = 0;
public static final int STATUS_ACCEPTED = 1;
public static final int STATUS_TERMINATED = 2;
public static final int STATUS_CANCELED = 3;
public static final int STATUS_FINISHED = 4;
public static final int STATUS_TRANSMITTING = 5;
public static final int STATUS_FAILED = 99;
protected static final int JINGLE_STATUS_INITIATED = 0;
protected static final int JINGLE_STATUS_ACCEPTED = 1;
protected static final int JINGLE_STATUS_TERMINATED = 2;
protected static final int JINGLE_STATUS_CANCELED = 3;
protected static final int JINGLE_STATUS_FINISHED = 4;
protected static final int JINGLE_STATUS_TRANSMITTING = 5;
protected static final int JINGLE_STATUS_FAILED = 99;
private int ibbBlockSize = 4096;
private int status = -1;
private int mJingleStatus = -1;
private int mStatus = -1;
private Message message;
private String sessionId;
private Account account;
@ -54,7 +56,7 @@ public class JingleConnection implements Downloadable {
private String transportId;
private Element fileOffer;
private JingleFile file = null;
private DownloadableFile file = null;
private String contentName;
private String contentCreator;
@ -71,11 +73,7 @@ public class JingleConnection implements Downloadable {
@Override
public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE_ERROR) {
if (initiator.equals(account.getFullJid())) {
mXmppConnectionService.markMessage(message,
Message.STATUS_SEND_FAILED);
}
status = STATUS_FAILED;
cancel();
}
}
};
@ -83,7 +81,7 @@ public class JingleConnection implements Downloadable {
final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
@Override
public void onFileTransmitted(JingleFile file) {
public void onFileTransmitted(DownloadableFile file) {
if (responder.equals(account.getFullJid())) {
sendSuccess();
if (acceptedAutomatically) {
@ -96,8 +94,8 @@ public class JingleConnection implements Downloadable {
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
message.setBody(Long.toString(file.getSize()) + ','
+ imageWidth + ',' + imageHeight);
message.setBody(Long.toString(file.getSize()) + '|'
+ imageWidth + '|' + imageHeight);
mXmppConnectionService.databaseBackend.createMessage(message);
mXmppConnectionService.markMessage(message,
Message.STATUS_RECEIVED);
@ -148,8 +146,8 @@ public class JingleConnection implements Downloadable {
return this.sessionId;
}
public String getAccountJid() {
return this.account.getFullJid();
public Account getAccount() {
return this.account;
}
public String getCounterPart() {
@ -253,13 +251,14 @@ public class JingleConnection implements Downloadable {
}
public void init(Account account, JinglePacket packet) {
this.status = STATUS_INITIATED;
this.mJingleStatus = JINGLE_STATUS_INITIATED;
Conversation conversation = this.mXmppConnectionService
.findOrCreateConversation(account,
packet.getFrom().split("/", 2)[0], false);
this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
this.message.setStatus(Message.STATUS_RECEIVED);
this.message.setType(Message.TYPE_IMAGE);
this.message.setStatus(Message.STATUS_RECEIVED_OFFER);
this.mStatus = Downloadable.STATUS_OFFER;
this.message.setDownloadable(this);
String[] fromParts = packet.getFrom().split("/", 2);
this.message.setPresence(fromParts[1]);
@ -304,7 +303,8 @@ public class JingleConnection implements Downloadable {
if (supportedFile) {
long size = Long.parseLong(fileSize.getContent());
message.setBody(Long.toString(size));
conversation.getMessages().add(message);
conversation.add(message);
mXmppConnectionService.updateConversationUi();
if (size <= this.mJingleConnectionManager
.getAutoAcceptFileSize()) {
Log.d(Config.LOGTAG, "auto accepting file from "
@ -323,7 +323,7 @@ public class JingleConnection implements Downloadable {
.push(message);
}
this.file = this.mXmppConnectionService.getFileBackend()
.getJingleFile(message, false);
.getFile(message, false);
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
byte[] key = conversation.getSymmetricKey();
if (key == null) {
@ -350,12 +350,13 @@ public class JingleConnection implements Downloadable {
}
private void sendInitRequest() {
this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED);
JinglePacket packet = this.bootstrapPacket("session-initiate");
Content content = new Content(this.contentCreator, this.contentName);
if (message.getType() == Message.TYPE_IMAGE) {
content.setTransportId(this.transportId);
this.file = this.mXmppConnectionService.getFileBackend()
.getJingleFile(message, false);
this.file = this.mXmppConnectionService.getFileBackend().getFile(
message, false);
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
Conversation conversation = this.message.getConversation();
this.mXmppConnectionService.renewSymmetricKey(conversation);
@ -369,7 +370,7 @@ public class JingleConnection implements Downloadable {
content.socks5transport().setChildren(getCandidatesAsElements());
packet.setContent(content);
this.sendJinglePacket(packet);
this.status = STATUS_INITIATED;
this.mJingleStatus = JINGLE_STATUS_INITIATED;
}
}
@ -382,8 +383,9 @@ public class JingleConnection implements Downloadable {
}
private void sendAccept() {
status = STATUS_ACCEPTED;
mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVING);
mJingleStatus = JINGLE_STATUS_ACCEPTED;
this.mStatus = Downloadable.STATUS_DOWNLOADING;
mXmppConnectionService.updateConversationUi();
this.mJingleConnectionManager.getPrimaryCandidate(this.account,
new OnPrimaryCandidateFound() {
@ -457,7 +459,7 @@ public class JingleConnection implements Downloadable {
Content content = packet.getJingleContent();
mergeCandidates(JingleCandidate.parse(content.socks5transport()
.getChildren()));
this.status = STATUS_ACCEPTED;
this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
this.connectNextCandidate();
return true;
@ -492,7 +494,8 @@ public class JingleConnection implements Downloadable {
} else if (content.socks5transport().hasChild("candidate-error")) {
Log.d(Config.LOGTAG, "received candidate error");
this.receivedCandidate = true;
if ((status == STATUS_ACCEPTED) && (this.sentCandidate)) {
if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
&& (this.sentCandidate)) {
this.connect();
}
return true;
@ -504,7 +507,8 @@ public class JingleConnection implements Downloadable {
JingleCandidate candidate = getCandidate(cid);
candidate.flagAsUsedByCounterpart();
this.receivedCandidate = true;
if ((status == STATUS_ACCEPTED) && (this.sentCandidate)) {
if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
&& (this.sentCandidate)) {
this.connect();
} else {
Log.d(Config.LOGTAG,
@ -532,7 +536,7 @@ public class JingleConnection implements Downloadable {
this.sendFallbackToIbb();
}
} else {
this.status = STATUS_TRANSMITTING;
this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
if (connection.needsActivation()) {
if (connection.getCandidate().isOurs()) {
Log.d(Config.LOGTAG, "candidate "
@ -619,13 +623,15 @@ public class JingleConnection implements Downloadable {
packet.setReason(reason);
this.sendJinglePacket(packet);
this.disconnect();
this.status = STATUS_FINISHED;
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_RECEIVED);
this.mJingleStatus = JINGLE_STATUS_FINISHED;
this.message.setStatus(Message.STATUS_RECEIVED);
this.message.setDownloadable(null);
this.mXmppConnectionService.updateMessage(message);
this.mJingleConnectionManager.finishConnection(this);
}
private void sendFallbackToIbb() {
Log.d(Config.LOGTAG, "sending fallback to ibb");
JinglePacket packet = this.bootstrapPacket("transport-replace");
Content content = new Content(this.contentCreator, this.contentName);
this.transportId = this.mJingleConnectionManager.nextRandomId();
@ -637,6 +643,7 @@ public class JingleConnection implements Downloadable {
}
private boolean receiveFallbackToIbb(JinglePacket packet) {
Log.d(Config.LOGTAG, "receiving fallack to ibb");
String receivedBlockSize = packet.getJingleContent().ibbTransport()
.getAttribute("block-size");
if (receivedBlockSize != null) {
@ -691,7 +698,7 @@ public class JingleConnection implements Downloadable {
}
private void receiveSuccess() {
this.status = STATUS_FINISHED;
this.mJingleStatus = JINGLE_STATUS_FINISHED;
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND);
this.disconnect();
@ -699,20 +706,15 @@ public class JingleConnection implements Downloadable {
}
public void cancel() {
this.status = STATUS_CANCELED;
this.mJingleStatus = JINGLE_STATUS_CANCELED;
this.disconnect();
if (this.message != null) {
if (this.responder.equals(account.getFullJid())) {
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_RECEPTION_FAILED);
this.mStatus = Downloadable.STATUS_FAILED;
this.mXmppConnectionService.updateConversationUi();
} else {
if (this.status == STATUS_INITIATED) {
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_REJECTED);
} else {
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_FAILED);
}
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_FAILED);
}
}
this.mJingleConnectionManager.finishConnection(this);
@ -789,7 +791,7 @@ public class JingleConnection implements Downloadable {
.setAttribute("cid", cid);
packet.setContent(content);
this.sentCandidate = true;
if ((receivedCandidate) && (status == STATUS_ACCEPTED)) {
if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
connect();
}
this.sendJinglePacket(packet);
@ -802,7 +804,7 @@ public class JingleConnection implements Downloadable {
content.socks5transport().addChild("candidate-error");
packet.setContent(content);
this.sentCandidate = true;
if ((receivedCandidate) && (status == STATUS_ACCEPTED)) {
if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
connect();
}
this.sendJinglePacket(packet);
@ -816,8 +818,8 @@ public class JingleConnection implements Downloadable {
return this.responder;
}
public int getStatus() {
return this.status;
public int getJingleStatus() {
return this.mJingleStatus;
}
private boolean equalCandidateExists(JingleCandidate candidate) {
@ -867,17 +869,34 @@ public class JingleConnection implements Downloadable {
return this.transport;
}
public void start() {
if (status == STATUS_INITIATED) {
new Thread(new Runnable() {
public boolean start() {
if (account.getStatus() == Account.STATUS_ONLINE) {
if (mJingleStatus == JINGLE_STATUS_INITIATED) {
new Thread(new Runnable() {
@Override
public void run() {
sendAccept();
}
}).start();
@Override
public void run() {
sendAccept();
}
}).start();
}
return true;
} else {
Log.d(Config.LOGTAG, "status (" + status + ") was not ok");
return false;
}
}
@Override
public int getStatus() {
return this.mStatus;
}
@Override
public long getFileSize() {
if (this.file != null) {
return this.file.getExpectedSize();
} else {
return 0;
}
}
}

View file

@ -10,16 +10,14 @@ import android.util.Log;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class JingleConnectionManager {
private XmppConnectionService xmppConnectionService;
public class JingleConnectionManager extends AbstractConnectionManager {
private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>();
private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>();
@ -28,7 +26,7 @@ public class JingleConnectionManager {
private SecureRandom random = new SecureRandom();
public JingleConnectionManager(XmppConnectionService service) {
this.xmppConnectionService = service;
super(service);
}
public void deliverPacket(Account account, JinglePacket packet) {
@ -38,7 +36,7 @@ public class JingleConnectionManager {
connections.add(connection);
} else {
for (JingleConnection connection : connections) {
if (connection.getAccountJid().equals(account.getFullJid())
if (connection.getAccount() == account
&& connection.getSessionId().equals(
packet.getSessionId())
&& connection.getCounterPart().equals(packet.getFrom())) {
@ -46,8 +44,13 @@ public class JingleConnectionManager {
return;
}
}
account.getXmppConnection().sendIqPacket(
packet.generateRespone(IqPacket.TYPE_ERROR), null);
IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR);
Element error = response.addChild("error");
error.setAttribute("type", "cancel");
error.addChild("item-not-found",
"urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild("unknown-session", "urn:xmpp:jingle:errors:1");
account.getXmppConnection().sendIqPacket(response, null);
}
}
@ -68,10 +71,6 @@ public class JingleConnectionManager {
this.connections.remove(connection);
}
public XmppConnectionService getXmppConnectionService() {
return this.xmppConnectionService;
}
public void getPrimaryCandidate(Account account,
final OnPrimaryCandidateFound listener) {
if (!this.primaryCandidates.containsKey(account.getJid())) {
@ -128,16 +127,6 @@ public class JingleConnectionManager {
return new BigInteger(50, random).toString(32);
}
public long getAutoAcceptFileSize() {
String config = this.xmppConnectionService.getPreferences().getString(
"auto_accept_file_size", "524288");
try {
return Long.parseLong(config);
} catch (NumberFormatException e) {
return 524288;
}
}
public void deliverIbbPacket(Account account, IqPacket packet) {
String sid = null;
Element payload = null;
@ -152,7 +141,8 @@ public class JingleConnectionManager {
}
if (sid != null) {
for (JingleConnection connection : connections) {
if (connection.hasTransportId(sid)) {
if (connection.getAccount() == account
&& connection.hasTransportId(sid)) {
JingleTransport transport = connection.getTransport();
if (transport instanceof JingleInbandTransport) {
JingleInbandTransport inbandTransport = (JingleInbandTransport) transport;
@ -170,7 +160,7 @@ public class JingleConnectionManager {
public void cancelInTransmission() {
for (JingleConnection connection : this.connections) {
if (connection.getStatus() == JingleConnection.STATUS_TRANSMITTING) {
if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
connection.cancel();
}
}

View file

@ -1,68 +0,0 @@
package eu.siacs.conversations.xmpp.jingle;
import java.io.File;
import java.security.Key;
import javax.crypto.spec.SecretKeySpec;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.CryptoHelper;
import android.util.Log;
public class JingleFile extends File {
private static final long serialVersionUID = 2247012619505115863L;
private long expectedSize = 0;
private String sha1sum;
private Key aeskey;
public JingleFile(String path) {
super(path);
}
public long getSize() {
return super.length();
}
public long getExpectedSize() {
if (this.aeskey != null) {
return (this.expectedSize / 16 + 1) * 16;
} else {
return this.expectedSize;
}
}
public void setExpectedSize(long size) {
this.expectedSize = size;
}
public String getSha1Sum() {
return this.sha1sum;
}
public void setSha1Sum(String sum) {
this.sha1sum = sum;
}
public void setKey(byte[] key) {
if (key.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) {
byte[] secretKey = new byte[16];
System.arraycopy(key, 0, secretKey, 0, 16);
this.aeskey = new SecretKeySpec(secretKey, "AES");
} else {
Log.d(Config.LOGTAG, "weird key");
}
Log.d(Config.LOGTAG,
"using aes key "
+ CryptoHelper.bytesToHex(this.aeskey.getEncoded()));
}
public Key getKey() {
return this.aeskey;
}
}

View file

@ -1,6 +1,5 @@
package eu.siacs.conversations.xmpp.jingle;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@ -10,6 +9,7 @@ import java.util.Arrays;
import android.util.Base64;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived;
@ -26,7 +26,7 @@ public class JingleInbandTransport extends JingleTransport {
private boolean established = false;
private JingleFile file;
private DownloadableFile file;
private InputStream fileInputStream = null;
private OutputStream fileOutputStream;
@ -77,7 +77,7 @@ public class JingleInbandTransport extends JingleTransport {
}
@Override
public void receive(JingleFile file,
public void receive(DownloadableFile file,
OnFileTransmissionStatusChanged callback) {
this.onFileTransmissionStatusChanged = callback;
this.file = file;
@ -86,7 +86,7 @@ public class JingleInbandTransport extends JingleTransport {
digest.reset();
file.getParentFile().mkdirs();
file.createNewFile();
this.fileOutputStream = getOutputStream(file);
this.fileOutputStream = file.createOutputStream();
if (this.fileOutputStream == null) {
callback.onFileTransferAborted();
return;
@ -100,20 +100,19 @@ public class JingleInbandTransport extends JingleTransport {
}
@Override
public void send(JingleFile file, OnFileTransmissionStatusChanged callback) {
public void send(DownloadableFile file,
OnFileTransmissionStatusChanged callback) {
this.onFileTransmissionStatusChanged = callback;
this.file = file;
try {
this.digest = MessageDigest.getInstance("SHA-1");
this.digest.reset();
fileInputStream = this.getInputStream(file);
fileInputStream = this.file.createInputStream();
if (fileInputStream == null) {
callback.onFileTransferAborted();
return;
}
this.sendNextBlock();
} catch (FileNotFoundException e) {
callback.onFileTransferAborted();
} catch (NoSuchAlgorithmException e) {
callback.onFileTransferAborted();
}

View file

@ -10,6 +10,7 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.utils.CryptoHelper;
public class JingleSocks5Transport extends JingleTransport {
@ -29,11 +30,11 @@ public class JingleSocks5Transport extends JingleTransport {
StringBuilder destBuilder = new StringBuilder();
destBuilder.append(jingleConnection.getSessionId());
if (candidate.isOurs()) {
destBuilder.append(jingleConnection.getAccountJid());
destBuilder.append(jingleConnection.getAccount().getFullJid());
destBuilder.append(jingleConnection.getCounterPart());
} else {
destBuilder.append(jingleConnection.getCounterPart());
destBuilder.append(jingleConnection.getAccountJid());
destBuilder.append(jingleConnection.getAccount().getFullJid());
}
mDigest.reset();
this.destination = CryptoHelper.bytesToHex(mDigest
@ -86,7 +87,7 @@ public class JingleSocks5Transport extends JingleTransport {
}
public void send(final JingleFile file,
public void send(final DownloadableFile file,
final OnFileTransmissionStatusChanged callback) {
new Thread(new Runnable() {
@ -96,7 +97,7 @@ public class JingleSocks5Transport extends JingleTransport {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
digest.reset();
fileInputStream = getInputStream(file);
fileInputStream = file.createInputStream();
if (fileInputStream == null) {
callback.onFileTransferAborted();
return;
@ -132,7 +133,7 @@ public class JingleSocks5Transport extends JingleTransport {
}
public void receive(final JingleFile file,
public void receive(final DownloadableFile file,
final OnFileTransmissionStatusChanged callback) {
new Thread(new Runnable() {
@ -145,7 +146,7 @@ public class JingleSocks5Transport extends JingleTransport {
socket.setSoTimeout(30000);
file.getParentFile().mkdirs();
file.createNewFile();
OutputStream fileOutputStream = getOutputStream(file);
OutputStream fileOutputStream = file.createOutputStream();
if (fileOutputStream == null) {
callback.onFileTransferAborted();
return;

View file

@ -1,88 +1,13 @@
package eu.siacs.conversations.xmpp.jingle;
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.CipherOutputStream;
import javax.crypto.CipherInputStream;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import eu.siacs.conversations.Config;
import android.util.Log;
import eu.siacs.conversations.entities.DownloadableFile;
public abstract class JingleTransport {
public abstract void connect(final OnTransportConnected callback);
public abstract void receive(final JingleFile file,
public abstract void receive(final DownloadableFile file,
final OnFileTransmissionStatusChanged callback);
public abstract void send(final JingleFile file,
public abstract void send(final DownloadableFile file,
final OnFileTransmissionStatusChanged callback);
private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
protected InputStream getInputStream(JingleFile file)
throws FileNotFoundException {
if (file.getKey() == null) {
return new FileInputStream(file);
} else {
try {
IvParameterSpec ips = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, file.getKey(), ips);
Log.d(Config.LOGTAG, "opening encrypted input stream");
return new CipherInputStream(new FileInputStream(file), 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;
}
}
}
protected OutputStream getOutputStream(JingleFile file)
throws FileNotFoundException {
if (file.getKey() == null) {
return new FileOutputStream(file);
} else {
try {
IvParameterSpec ips = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, file.getKey(), ips);
Log.d(Config.LOGTAG, "opening encrypted output stream");
return new CipherOutputStream(new FileOutputStream(file),
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;
}
}
}
}

View file

@ -1,7 +1,9 @@
package eu.siacs.conversations.xmpp.jingle;
import eu.siacs.conversations.entities.DownloadableFile;
public interface OnFileTransmissionStatusChanged {
public void onFileTransmitted(JingleFile file);
public void onFileTransmitted(DownloadableFile file);
public void onFileTransferAborted();
}

View file

@ -1,7 +1,7 @@
package eu.siacs.conversations.xmpp.jingle.stanzas;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.jingle.JingleFile;
public class Content extends Element {
@ -25,7 +25,7 @@ public class Content extends Element {
this.transportId = sid;
}
public void setFileOffer(JingleFile actualFile, boolean otr) {
public void setFileOffer(DownloadableFile actualFile, boolean otr) {
Element description = this.addChild("description",
"urn:xmpp:jingle:apps:file-transfer:3");
Element offer = description.addChild("offer");