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 url = https://github.com/open-keychain/openpgp-api-lib.git
[submodule "libs/MemorizingTrustManager"] [submodule "libs/MemorizingTrustManager"]
path = 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"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.siacs.conversations" package="eu.siacs.conversations"
android:versionCode="31" android:versionCode="32"
android:versionName="0.7.3" > android:versionName="0.8-alpha" >
<uses-sdk <uses-sdk
android:minSdkVersion="14" android:minSdkVersion="14"
@ -22,14 +23,10 @@
android:allowBackup="true" android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:icon="@drawable/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
tools:replace="android:label"
android:theme="@style/ConversationsTheme" > android:theme="@style/ConversationsTheme" >
<service android:name="eu.siacs.conversations.services.XmppConnectionService" /> <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" > <receiver android:name="eu.siacs.conversations.services.EventReceiver" >
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />

325
README.md
View file

@ -1,19 +1,22 @@
#Conversations # Conversations
Conversations - the very last word in instant messaging
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) [![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) ![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 * Be as beautiful and easy to use as possible without sacrificing security or
privacy privacy
* Rely on existing, well established protocols (XMPP) * Rely on existing, well established protocols (XMPP)
* Do not require a Google Account or specifically Google Cloud Messaging (GCM) * 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 ## Features
* End-to-end encryption with either OTR or openPGP
* End-to-end encryption with either [OTR](https://otr.cypherpunks.ca/) or [OpenPGP](http://www.openpgp.org/about_openpgp/)
* Sending and receiving images * Sending and receiving images
* Indication when your contact has read your message * Indication when your contact has read your message
* Intuitive UI that follows Android Design guidelines * Intuitive UI that follows Android Design guidelines
@ -21,47 +24,56 @@ Conversations - the very last word in instant messaging
* Syncs with desktop client * Syncs with desktop client
* Conferences (with support for bookmarks) * Conferences (with support for bookmarks)
* Address book integration * Address book integration
* Multiple Accounts / unified inbox * Multiple accounts / unified inbox
* Very low impact on battery life * Very low impact on battery life
###XMPP Features ### 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 works with every XMPP server out there. However XMPP is an
Conversations supports a couple of those to make the overall user experience better. There is a extensible protocol. These extensions are standardized as well in so called
chance that your current XMPP server does not support these extensions. XEP's. Conversations supports a couple of these to make the overall user
Therefore to get the most out of Conversations you should consider either switching to an experience better. There is a chance that your current XMPP server does not
XMPP server that does or - even better - run your own XMPP server for you and support these extensions; therefore to get the most out of Conversations you
your friends. should consider either switching to an XMPP server that does or — even better —
These XEPs are - as of now: run your own XMPP server for you and your friends. These XEP's are:
* XEP-0065: SOCKS5 Bytestreams - or rather mod_proxy65. Will be used to transfer files if both parties are behind a firewall (NAT).
* 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-0138: Stream Compression saves bandwidth
* XEP-0163: Personal Eventing Protocol for avatars * 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 * 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 your desktop client and thus allows you to switch seamlessly from your mobile
client to your desktop client and back within one conversation. client to your desktop client and back within one conversation.
* XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections * XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections
* XEP-0352: Client State Indication let the server know whether or not * XEP-0352: Client State Indication let the server know whether or not
Conversations is in the background. Allows the server to save bandwidth by 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) * [Daniel Gultsch](https://github.com/inputmice)
####Code Contributions #### Code Contributions
(In order of appearance) (In order of appearance)
* [Rene Treffer](https://github.com/rtreffer) * [Rene Treffer](https://github.com/rtreffer)
* [Andreas Straub](https://github.com/strb) * [Andreas Straub](https://github.com/strb)
* [Alethea Butler](https://github.com/alethea) * [Alethea Butler](https://github.com/alethea)
* [M. Dietrich](https://github.com/emdete) * [M. Dietrich](https://github.com/emdete)
* [betheg](https://github.com/betheg) * [betheg](https://github.com/betheg)
####Logo #### Logo
* [Diego Turtulici](http://efesto.eigenlab.org/~diesys) * [Diego Turtulici](http://efesto.eigenlab.org/~diesys)
####Translations #### Translations
* [Sergio Cárdenas](https://github.com/kruks23) (Spanish) * [Sergio Cárdenas](https://github.com/kruks23) (Spanish)
* [Benoit Bouvarel](https://github.com/BenoitBouvarel) (French) * [Benoit Bouvarel](https://github.com/BenoitBouvarel) (French)
* [Daniel Gultsch](https://github.com/iNPUTmice) (German) * [Daniel Gultsch](https://github.com/iNPUTmice) (German)
@ -71,180 +83,215 @@ These XEPs are - as of now:
* [Anders Sandblad](https://github.com/andersruneson) (Swedish) * [Anders Sandblad](https://github.com/andersruneson) (Swedish)
* [Aizaz AZ](http://www.linkedin.com/in/aizazhaider) (Chinese) * [Aizaz AZ](http://www.linkedin.com/in/aizazhaider) (Chinese)
##FAQ ## FAQ
###General
####How do I install Conversations? ### General
#### How do I install Conversations?
Conversations is entirely open source and licensed under GPLv3. So if you are a 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. build your apk file.
The more convenient way - which not only gives you automatic updates but also 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 supports the further development of Conversations - is to buy the App in the
[Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations). 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. #### 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) [![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 **Disclaimer:** I'm not a huge fan of PayPal and their business policies. For
contributions please get in touch with me beforehand and we can talk about bank larger contributions please get in touch with me beforehand and we can talk
transfer (SEPA). 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? #### 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.
####Conversations doesn't work for me. Where can I get help? XMPP, like email, is a federated protocol which means that there is not one
You can join our conference room on conversations@conference.siacs.eu A lot of 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 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 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 you found a bug or your app crashes please read the Developer / Report Bugs
section of this document. section of this document.
####I need professional support with Conversations or setting up my server #### I need professional support with Conversations or setting up my server
I'm available for hire. Contact me at inputmice@siacs.eu
####How does the address book integration work? I'm available for hire. Contact me at `inputmice@siacs.eu`.
The address bock integration was designed to protect your privacy. Conversations
#### 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 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 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 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 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 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 details within Conversations. This will start an "add to address book" intent
as payload. This doesnt require Conversations to have write permissions on your with the JID as the payload. This doesn't require Conversations to have write
address book but also doesnt require you to copy past Jabber ID from one app to permissions on your address book but also doesn't require you to copy/paste a
another. JID from one app to another.
####I get 'delivery failed' on my messages #### 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 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. later time.
For text messages the answer to your question is a little bit more complex. 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 When you see 'delivery failed' on text messages, it is always something that is
the server. The most common reason for this is that the recipient failed to being reported by the server. The most common reason for this is that the
resume a connection. When a client loses connectivity for a short time the client recipient failed to resume a connection. When a client loses connectivity for a
usually has a five minute window to pick up that connection again. When the short time the client usually has a five minute window to pick up that
client fails to do so because the network connectivity is out for longer than connection again. When the client fails to do so because the network
that all messages sent to that client will be returned to the sender resulting connectivity is out for longer than that all messages sent to that client will
in a delivery failed. be returned to the sender resulting in a delivery failed.
Other less common reasons are that the message you sent didnt meet some Other less common reasons are that the message you sent didn't meet some
criterias enforced by the server. (Too large, too many) Another reason could be criteria enforced by the server (too large, too many). Another reason could be
that the recipient is offline and the server doesnt provide offline storage. 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 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 the first one happens always after some time and the second one happens almost
instantly. instantly.
####Where can I see the status of my contacts? How can I set a status or priority #### 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
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 works because users are either lazy or just forget about them. Setting them
automatically does not provide quality results either. Keyboard or mouse automatically does not provide quality results either. Keyboard or mouse
activity as indicator for example fails when the user is just looking at activity as indicator for example fails when the user is just looking at
something (reading an article, watching a movie). Furthermore automatic setting 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 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 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 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** (XEP-0333, supported by Conversations since 0.4) we have the ability to **know**
whether or not your messages are being read. whether or not your messages are being read. Similar things can be said for
Similar things can be said for priorities. In the past priorities have been used priorities. In the past priorities have been used (by servers, not by clients!)
(By servers, not by clients!) to route your messages to one specific client. to route your messages to one specific client. With carbon messages (XEP-0280,
With carbon messages (XEP-0280, supported by Conversations since 0.1) this is no supported by Conversations since 0.1) this is no longer necessary. Using
longer necessary. Using priorities to route OTR messages isn't pratical either priorities to route OTR messages isn't practical either because they are not
because they are not changeable on the fly. Metrics like last active client changeable on the fly. Metrics like last active client (the client which sent
(the client which sent the last message) are much better. the last message) are much better.
Unfortunately these modern replacements for legacy XMPP features are not widely Unfortunately these modern replacements for legacy XMPP features are not widely
adopted. However Conversations should be an instant messenger for the future and adopted. However Conversations should be an instant messenger for the future and
instead of making Conversations compatible with the past we should work on 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 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 Conversations is trying to get rid of old behaviours and set an example for
other clients. other clients.
####Conversations is missing a certain feature #### 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 I'm open for new feature suggestions. You can use the [issue tracker][issues] on
already suggested it. Be assured that I read each and every ticket. If I like it GitHub. Please take some time to browse through the issues to see if someone
I will leave it open until it's implemented. If I don't like it I will close else already suggested it. Be assured that I read each and every ticket. If I
it. (Usually with a short comment). If I don't comment on an feature request like it I will leave it open until it's implemented. If I don't like it I will
that's probably a good sign because this means I agree with you. Commenting with close it (usually with a short comment). If I don't comment on an feature
+1 on either open or closed issues won't change my mind nor will it accelerate the request that's probably a good sign because this means I agree with you.
development. 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 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. 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 need a feature and I need it now!
I am available for hire. Contact me JID: inputmice@siacs.eu
###Security I am available for hire. Contact me via XMPP: `inputmice@siacs.eu`
####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.
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. ### Security
####How does the encryption for conferences work?
For conferences the only supported encryption method is OpenPGP. (OTR does not #### Why are there two end-to-end encryption methods and which one should I choose?
work with multiple participants.) Every participant has to announce their
OpenPGP key. (See answer above). If you would like to send encrypted messages to 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 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 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 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 touch every key id (The hexadecimal number below a contact). This will send you
to OpenKeychain which will assist you on adding the key. to OpenKeychain which will assist you on adding the key. This works best in
This works best in very small conferences with contacts you are already using very small conferences with contacts you are already using OpenPGP with. This
OpenPGP with. This feature is regarded experimental. Conversations is the only feature is regarded experimental. Conversations is the only client that uses
client that uses XEP-0027 with conferences. (The XEP neither specifically allows XEP-0027 with conferences. (The XEP neither specifically allows nor disallows
nor disallows this.) this.)
###Development
####How do I build Conversations ### Development
#### How do I build Conversations
Make sure to have ANDROID_HOME point to your Android SDK Make sure to have ANDROID_HOME point to your Android SDK
```
git clone https://github.com/siacs/Conversations.git git clone https://github.com/siacs/Conversations.git
cd Conversations cd Conversations
git submodule update --init --recursive git submodule update --init --recursive
ant clean ant clean
ant debug ant debug
```
####How do I debug Conversations #### How do I debug Conversations
If something goes wrong Conversations usually exposes very little information in If something goes wrong Conversations usually exposes very little information in
the UI. (Other than the fact that something didn't work) the UI (other than the fact that something didn't work). However with adb
However with adb (android debug bridge) you squeeze some more information out of (android debug bridge) you squeeze some more information out of Conversations.
Conversations. These information are especially useful if you are experiencing These information are especially useful if you are experiencing trouble with
troubles with your connection or with file transfer. your connection or with file transfer.
````
adb -d logcat -v time -s conversations adb -d logcat -v time -s conversations
````
####I found a bug #### 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 Please report it to our [issue tracker][issues]. If your app crashes please
steps to reproduce. provide a stack trace. If you are experiencing misbehaviour please provide
Always mention whether you are running the latest Play Store version or the detailed steps to reproduce. Always mention whether you are running the latest
current HEAD. Play Store version or the current HEAD. If you are having problems connecting to
If you are having problems connecting to your XMPP server your file transfer your XMPP server your file transfer doesnt work as expected please always
doesnt work as expected please always include a logcat debug output with your include a logcat debug output with your issue (see above).
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 #!/bin/env ruby
resolutions={'mdpi'=> 1, 'hdpi' => 1.5, 'xhdpi' => 2, 'xxhdpi' => 3} resolutions={
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] } '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| images.each do |source, result|
resolutions.each do |name, factor| resolutions.each do |name, factor|
size = factor * result[1] 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"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="match_parent" android:layout_height="fill_parent"
android:background="@color/primarybackground" > android:background="@color/secondarybackground" >
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" > 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 <RelativeLayout
android:layout_width="wrap_content" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="88dp" android:layout_margin="8dp"
android:padding="8dp" > android:background="@drawable/infocard_border"
android:padding="16dp" >
<QuickContactBadge <QuickContactBadge
android:id="@+id/details_contact_badge" android:id="@+id/details_contact_badge"
android:layout_width="72dp" android:layout_width="72dp"
android:layout_height="72dp" android:layout_height="72dp"
android:layout_centerVertical="true" android:layout_alignParentTop="true"
android:scaleType="centerCrop" /> android:scaleType="centerCrop" />
<LinearLayout <LinearLayout
android:id="@+id/details_jidbox" android:id="@+id/details_jidbox"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_marginLeft="16dp"
android:layout_toRightOf="@+id/details_contact_badge" android:layout_toRightOf="@+id/details_contact_badge"
android:orientation="vertical" android:orientation="vertical" >
android:paddingLeft="8dp" >
<TextView <TextView
android:id="@+id/details_contactjid" android:id="@+id/details_contactjid"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="8dp" android:text="@string/account_settings_example_jabber_id"
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:textColor="@color/primarytext" android:textColor="@color/primarytext"
android:textSize="?attr/TextSizeHeadline" android:textSize="?attr/TextSizeHeadline"
android:textStyle="bold" /> android:textStyle="bold" />
<TextView <LinearLayout
android:id="@+id/details_lastseen"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingLeft="8dp" android:orientation="horizontal" >
android:singleLine="true"
<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:textColor="@color/primarytext"
android:textSize="?attr/TextSizeBody" /> android:textSize="?attr/TextSizeBody" />
</LinearLayout> </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> </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 <LinearLayout
android:id="@+id/details_contact_keys" android:id="@+id/details_contact_keys"
android:layout_width="match_parent" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="8dp"
android:background="@drawable/infocard_border"
android:divider="?android:dividerHorizontal" android:divider="?android:dividerHorizontal"
android:orientation="vertical" android:orientation="vertical"
android:padding="8dp"
android:showDividers="middle" > android:showDividers="middle" >
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

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

View file

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

View file

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

View file

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

View file

@ -15,7 +15,8 @@
android:layout_alignParentBottom="true" android:layout_alignParentBottom="true"
android:layout_toLeftOf="@+id/message_photo" android:layout_toLeftOf="@+id/message_photo"
android:background="@drawable/message_border" android:background="@drawable/message_border"
android:minHeight="48dp" > android:minHeight="48dp"
android:longClickable="true">
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -43,8 +44,15 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:autoLink="web" android:autoLink="web"
android:textColor="@color/primarytext" android:textColor="@color/primarytext"
android:textIsSelectable="true"
android:textSize="?attr/TextSizeBody" /> 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 <LinearLayout
android:layout_width="wrap_content" android:layout_width="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>
<string-array name="mute_options_descriptions"> <string-array name="mute_options_descriptions">
<item>30 Minuten</item> <item>30 Minuten</item>
<item>eine Stunde</item> <item>1 Stunde</item>
<item>2 Stunden</item> <item>2 Stunden</item>
<item>8 Stunden</item> <item>8 Stunden</item>
<item>bis auf Widerruf</item> <item>bis auf Widerruf</item>

View file

@ -51,7 +51,7 @@
<string name="send_now">Jetzt abschicken</string> <string name="send_now">Jetzt abschicken</string>
<string name="send_never">Nie mehr nachfragen</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_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="touch_to_fix">Drücke hier, um das Konto zu verwalten</string>
<string name="attach_file">Datei anfügen</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> <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="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="delete_messages">Nachrichten löschen</string>
<string name="also_end_conversation">Diese Unterhaltung danach beenden</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_plain_text_message">Unverschlüsselt schreiben</string>
<string name="send_otr_message">OTR-verschlüsselt schreiben</string> <string name="send_otr_message">OTR-verschlüsselt schreiben</string>
<string name="send_pgp_message">OpenPGP-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="offering">angeboten&#8230;</string>
<string name="waiting">warten&#8230;</string> <string name="waiting">warten&#8230;</string>
<string name="no_pgp_key">Kein OpenPGP-Schlüssel gefunden</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="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_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="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> <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">Klingelton</string>
<string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</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">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">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_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> <string name="pref_advanced_options">Erweiterte Optionen</string>
@ -233,7 +233,7 @@
<string name="skip">Überspringen</string> <string name="skip">Überspringen</string>
<string name="pref_ui_options">Benutzeroberfläche</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">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">Benachrichtigungen deaktivieren</string>
<string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string> <string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string>
<string name="notifications_disabled">Benachrichtigungen sind deaktiviert</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_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">Verschlüsselte Nachrichten nicht speichern</string>
<string name="pref_dont_save_encrypted_summary">Achtung: Kann zu Nachrichtenverlust führen</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">Einstellungen für Experten</string>
<string name="pref_expert_options_summary">Hier bitte vorsichtig sein</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_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">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> <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_banned">Du wurdest aus dem Konferenzraum verbannt</string>
<string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</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="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> </resources>

View file

@ -250,6 +250,8 @@
<string name="pref_force_encryption_summary">Siempre enviar mensajes encriptados (excepto para conferencias)</string> <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">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_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">Ajustes avanzados</string>
<string name="pref_expert_options_summary">Por favor, cuidado con estas opciones</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> <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_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_members_only">Esta conferencia es solo para miembros</string>
<string name="conference_kicked">Has sido expulsado de esta conferencia</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> </resources>

View file

@ -205,7 +205,7 @@
<string name="conference_address">Konferentziaren helbidea</string> <string name="conference_address">Konferentziaren helbidea</string>
<string name="conference_address_example">gela@conference.example.com</string> <string name="conference_address_example">gela@conference.example.com</string>
<string name="save_as_bookmark">Gorde laster-marka bezala</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="bookmark_already_exists">Laster-marka hau existitzen da dagoeneko</string>
<string name="you">Zu</string> <string name="you">Zu</string>
<string name="action_edit_subject">Konferentziaren gaia editatu</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_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">Ez gorde enkriptatutako mezuak</string>
<string name="pref_dont_save_encrypted_summary">Adi: Honek mezuen galera ekar lezake</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">Adituentzako aukerak</string>
<string name="pref_expert_options_summary">Mesedez kontuz ibili hauekin</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">Letraren tamaina handitu</string>
<string name="pref_use_larger_font_summary">Letra tamaina handiagoa erabili aplikazio osoan zehar</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_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_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> </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_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="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="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">Ressource XMPP</string>
<string name="pref_xmpp_resource_summary">Nom permettant d\'identifier ce client 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> <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_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">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_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="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="error_decrypting_file">Erreur d\'E/S lors du déchiffrement du fichier</string>
<string name="accept">Accepter</string> <string name="accept">Accepter</string>
@ -145,6 +147,8 @@
<string name="mgmt_account_edit">Modifier le compte</string> <string name="mgmt_account_edit">Modifier le compte</string>
<string name="mgmt_account_delete">Supprimer</string> <string name="mgmt_account_delete">Supprimer</string>
<string name="mgmt_account_disable">Désactiver temporairement</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_enable">Activer</string>
<string name="mgmt_account_are_you_sure">Êtes-vous sûr?</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> <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="muc_details_other_members">Autres membres</string>
<string name="server_info_carbon_messages">Copies carbone</string> <string name="server_info_carbon_messages">Copies carbone</string>
<string name="server_info_stream_management">Gestion des flux</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="missing_public_keys">Aucune annonce de clef publique</string>
<string name="last_seen_now">en ligne à l\'instant</string> <string name="last_seen_now">en ligne à l\'instant</string>
<string name="last_seen_min">en ligne il y a 1 minute</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="contact_added_you">Votre correspondant vous a ajouté dans sa liste de contacts</string>
<string name="add_back">Ajouter également</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="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> </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>
<string-array name="mute_options_descriptions"> <string-array name="mute_options_descriptions">
<item>30 minutes</item> <item>30 minutes</item>
<item>one hour</item> <item>1 hour</item>
<item>2 hours</item> <item>2 hours</item>
<item>8 hours</item> <item>8 hours</item>
<item>until further notice</item> <item>until further notice</item>
@ -36,4 +36,4 @@
<item>-1</item> <item>-1</item>
</integer-array> </integer-array>
</resources> </resources>

View file

@ -250,13 +250,15 @@
<string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string> <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">Dont save encrypted messages</string>
<string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</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">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">Increase font size</string>
<string name="pref_use_larger_font_summary">Use larger font sizes across the entire app</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_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">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_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_expert_options_other">Other</string>
<string name="pref_conference_name">Conference name</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_banned">You are banned from this conference</string>
<string name="conference_members_only">This conference is members only</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="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> </resources>

View file

@ -1,15 +1,4 @@
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <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"> <style name="Divider">
<item name="android:layout_width">match_parent</item> <item name="android:layout_width">match_parent</item>
<item name="android:layout_height">1.5dp</item> <item name="android:layout_height">1.5dp</item>

View file

@ -51,6 +51,7 @@
android:title="@string/pref_sound" /> android:title="@string/pref_sound" />
<CheckBoxPreference <CheckBoxPreference
android:defaultValue="true"
android:dependency="show_notification" android:dependency="show_notification"
android:key="always_notify_in_conference" android:key="always_notify_in_conference"
android:summary="@string/pref_conference_notifications_summary" android:summary="@string/pref_conference_notifications_summary"
@ -88,6 +89,11 @@
android:key="dont_save_encrypted" android:key="dont_save_encrypted"
android:summary="@string/pref_dont_save_encrypted_summary" android:summary="@string/pref_dont_save_encrypted_summary"
android:title="@string/pref_dont_save_encrypted" /> 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>
<PreferenceCategory android:title="@string/pref_expert_options_other" > <PreferenceCategory android:title="@string/pref_expert_options_other" >
<CheckBoxPreference <CheckBoxPreference
@ -105,4 +111,4 @@
android:title="@string/pref_never_send_crash" /> android:title="@string/pref_never_send_crash" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

View file

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

View file

@ -11,16 +11,14 @@ import net.java.otr4j.crypto.OtrCryptoException;
import org.json.JSONException; import org.json.JSONException;
import org.json.JSONObject; import org.json.JSONObject;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.crypto.OtrEngine; import eu.siacs.conversations.crypto.OtrEngine;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.os.SystemClock;
public class Account extends AbstractEntity { public class Account extends AbstractEntity {
@ -64,16 +62,14 @@ public class Account extends AbstractEntity {
protected boolean online = false; protected boolean online = false;
transient OtrEngine otrEngine = null; private OtrEngine otrEngine = null;
transient XmppConnection xmppConnection = null; private XmppConnection xmppConnection = null;
transient protected Presences presences = new Presences(); private Presences presences = new Presences();
private long mEndGracePeriod = 0L;
private String otrFingerprint; private String otrFingerprint;
private Roster roster = null; private Roster roster = null;
private List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>(); private List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>();
public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<Conversation>(); public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<Conversation>();
public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<Conversation>(); public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<Conversation>();
@ -160,8 +156,12 @@ public class Account extends AbstractEntity {
} }
public boolean hasErrorStatus() { public boolean hasErrorStatus() {
return getStatus() > STATUS_NO_INTERNET if (getXmppConnection() == null) {
&& (getXmppConnection().getAttempt() >= 2); return false;
} else {
return getStatus() > STATUS_NO_INTERNET
&& (getXmppConnection().getAttempt() >= 2);
}
} }
public void setResource(String resource) { public void setResource(String resource) {
@ -341,20 +341,6 @@ public class Account extends AbstractEntity {
return false; 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) { public boolean setAvatar(String filename) {
if (this.avatar != null && this.avatar.equals(filename)) { if (this.avatar != null && this.avatar.equals(filename)) {
return false; return false;
@ -397,4 +383,17 @@ public class Account extends AbstractEntity {
return R.string.account_status_unknown; 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 java.util.Locale;
import android.content.Context;
import android.graphics.Bitmap;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
public class Bookmark extends Element implements ListItem { public class Bookmark extends Element implements ListItem {
@ -120,21 +117,14 @@ public class Bookmark extends Element implements ListItem {
return this.account; 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) { public void setConversation(Conversation conversation) {
this.mJoinedConversation = conversation; this.mJoinedConversation = conversation;
} }
public Conversation getConversation() {
return this.mJoinedConversation;
}
public String getName() { public String getName() {
return this.getAttribute("name"); return this.getAttribute("name");
} }

View file

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

View file

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

View file

@ -1,23 +1,21 @@
package eu.siacs.conversations.entities; 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.Config;
import eu.siacs.conversations.R;
import android.content.ContentValues; import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
public class Message extends AbstractEntity { public class Message extends AbstractEntity {
public static final String TABLENAME = "messages"; 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_RECEIVED = 0;
public static final int STATUS_UNSEND = 1; public static final int STATUS_UNSEND = 1;
public static final int STATUS_SEND = 2; public static final int STATUS_SEND = 2;
public static final int STATUS_SEND_FAILED = 3; 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_WAITING = 5;
public static final int STATUS_OFFERED = 6; public static final int STATUS_OFFERED = 6;
public static final int STATUS_SEND_RECEIVED = 7; public static final int STATUS_SEND_RECEIVED = 7;
@ -61,6 +59,9 @@ public class Message extends AbstractEntity {
protected Downloadable downloadable = null; protected Downloadable downloadable = null;
public boolean markable = false; public boolean markable = false;
private Message mNextMessage = null;
private Message mPreviousMessage = null;
private Message() { private Message() {
} }
@ -131,14 +132,8 @@ public class Message extends AbstractEntity {
if (this.trueCounterpart == null) { if (this.trueCounterpart == null) {
return null; return null;
} else { } else {
Account account = this.conversation.getAccount(); return this.conversation.getAccount().getRoster()
Contact contact = account.getRoster().getContact( .getContactFromRoster(this.trueCounterpart);
this.trueCounterpart);
if (contact.showInRoster()) {
return contact;
} else {
return null;
}
} }
} }
} }
@ -147,22 +142,6 @@ public class Message extends AbstractEntity {
return body; 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() { public long getTimeSent() {
return timeSent; return timeSent;
} }
@ -301,21 +280,34 @@ public class Message extends AbstractEntity {
} }
public Message next() { public Message next() {
int index = this.conversation.getMessages().indexOf(this); if (this.mNextMessage == null) {
if (index < 0 || index >= this.conversation.getMessages().size() - 1) { synchronized (this.conversation.messages) {
return null; int index = this.conversation.messages.indexOf(this);
} else { if (index < 0
return this.conversation.getMessages().get(index + 1); || index >= this.conversation.getMessages().size() - 1) {
this.mNextMessage = null;
} else {
this.mNextMessage = this.conversation.messages
.get(index + 1);
}
}
} }
return this.mNextMessage;
} }
public Message prev() { public Message prev() {
int index = this.conversation.getMessages().indexOf(this); if (this.mPreviousMessage == null) {
if (index <= 0 || index > this.conversation.getMessages().size()) { synchronized (this.conversation.messages) {
return null; int index = this.conversation.messages.indexOf(this);
} else { if (index <= 0 || index > this.conversation.messages.size()) {
return this.conversation.getMessages().get(index - 1); this.mPreviousMessage = null;
} else {
this.mPreviousMessage = this.conversation.messages
.get(index - 1);
}
}
} }
return this.mPreviousMessage;
} }
public boolean mergable(Message message) { public boolean mergable(Message message) {
@ -323,6 +315,8 @@ public class Message extends AbstractEntity {
return false; return false;
} }
return (message.getType() == Message.TYPE_TEXT return (message.getType() == Message.TYPE_TEXT
&& this.getDownloadable() == null
&& message.getDownloadable() == null
&& message.getEncryption() != Message.ENCRYPTION_PGP && message.getEncryption() != Message.ENCRYPTION_PGP
&& this.getType() == message.getType() && this.getType() == message.getType()
&& this.getEncryption() == message.getEncryption() && this.getEncryption() == message.getEncryption()
@ -332,7 +326,9 @@ public class Message extends AbstractEntity {
.getStatus() == Message.STATUS_SEND_RECEIVED) && (message .getStatus() == Message.STATUS_SEND_RECEIVED) && (message
.getStatus() == Message.STATUS_UNSEND .getStatus() == Message.STATUS_UNSEND
|| message.getStatus() == Message.STATUS_SEND || message || message.getStatus() == Message.STATUS_SEND || message
.getStatus() == Message.STATUS_SEND_DISPLAYED))))); .getStatus() == Message.STATUS_SEND_DISPLAYED))))
&& !message.bodyContainsDownloadable()
&& !this.bodyContainsDownloadable());
} }
public String getMergedBody() { public String getMergedBody() {
@ -369,4 +365,148 @@ public class Message extends AbstractEntity {
return prev.mergable(this); 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() { public long getPgpKeyId() {
return this.pgpKeyId; return this.pgpKeyId;
} }
public Contact getContact() {
return account.getRoster().getContactFromRoster(getJid());
}
} }
private Account account; private Account account;
@ -116,8 +120,9 @@ public class MucOptions {
private String joinnick; private String joinnick;
private String password = null; private String password = null;
public MucOptions(Account account) { public MucOptions(Conversation conversation) {
this.account = account; this.account = conversation.getAccount();
this.conversation = conversation;
} }
public void deleteUser(String name) { public void deleteUser(String name) {
@ -253,10 +258,6 @@ public class MucOptions {
this.joinnick = nick; this.joinnick = nick;
} }
public void setConversation(Conversation conversation) {
this.conversation = conversation;
}
public boolean online() { public boolean online() {
return this.isOnline; return this.isOnline;
} }
@ -361,4 +362,8 @@ public class MucOptions {
conversation conversation
.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password); .setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
} }
public Conversation getConversation() {
return this.conversation;
}
} }

View file

@ -14,7 +14,10 @@ public class Roster {
this.account = account; this.account = account;
} }
public Contact getContactAsShownInRoster(String jid) { public Contact getContactFromRoster(String jid) {
if (jid == null) {
return null;
}
String cleanJid = jid.split("/", 2)[0]; String cleanJid = jid.split("/", 2)[0];
Contact contact = contacts.get(cleanJid); Contact contact = contacts.get(cleanJid);
if (contact != null && contact.showInRoster()) { 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 null;
} }
} }
return finishedMessage; return finishedMessage;
} }
@ -348,10 +347,18 @@ public class MessageParser extends AbstractParser implements
mXmppConnectionService.databaseBackend mXmppConnectionService.databaseBackend
.updateAccount(account); .updateAccount(account);
} }
mXmppConnectionService.getAvatarService().clear(
account);
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateAccountUi();
} else { } else {
Contact contact = account.getRoster().getContact( Contact contact = account.getRoster().getContact(
from); from);
contact.setAvatar(avatar.getFilename()); contact.setAvatar(avatar.getFilename());
mXmppConnectionService.getAvatarService().clear(
contact);
mXmppConnectionService.updateConversationUi();
mXmppConnectionService.updateRosterUi();
} }
} else { } else {
mXmppConnectionService.fetchAvatar(account, avatar); mXmppConnectionService.fetchAvatar(account, avatar);
@ -417,8 +424,7 @@ public class MessageParser extends AbstractParser implements
message = this.parseCarbonMessage(packet, account); message = this.parseCarbonMessage(packet, account);
if (message != null) { if (message != null) {
if (message.getStatus() == Message.STATUS_SEND) { if (message.getStatus() == Message.STATUS_SEND) {
mXmppConnectionService.getNotificationService() account.activateGracePeriod();
.activateGracePeriod();
notify = false; notify = false;
mXmppConnectionService.markRead( mXmppConnectionService.markRead(
message.getConversation(), false); message.getConversation(), false);
@ -440,8 +446,7 @@ public class MessageParser extends AbstractParser implements
} else { } else {
mXmppConnectionService.markRead(message.getConversation(), mXmppConnectionService.markRead(message.getConversation(),
false); false);
mXmppConnectionService.getNotificationService() account.activateGracePeriod();
.activateGracePeriod();
notify = false; notify = false;
} }
} }
@ -471,13 +476,26 @@ public class MessageParser extends AbstractParser implements
} }
} }
Conversation conversation = message.getConversation(); 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 (packet.getType() != MessagePacket.TYPE_ERROR) {
if (message.getEncryption() == Message.ENCRYPTION_NONE if (message.getEncryption() == Message.ENCRYPTION_NONE
|| mXmppConnectionService.saveEncryptedMessages()) { || mXmppConnectionService.saveEncryptedMessages()) {
mXmppConnectionService.databaseBackend.createMessage(message); mXmppConnectionService.databaseBackend.createMessage(message);
} }
} }
if (message.trusted() && message.bodyContainsDownloadable()) {
this.mXmppConnectionService.getHttpConnectionManager()
.createNewConnection(message);
notify = false;
}
notify = notify && !conversation.isMuted(); notify = notify && !conversation.isMuted();
if (notify) { if (notify) {
mXmppConnectionService.getNotificationService().push(message); mXmppConnectionService.getNotificationService().push(message);

View file

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

View file

@ -11,6 +11,7 @@ import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Roster; import eu.siacs.conversations.entities.Roster;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.database.sqlite.SQLiteCantOpenDatabaseException;
import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteOpenHelper;
@ -151,14 +152,13 @@ public class DatabaseBackend extends SQLiteOpenHelper {
return list; return list;
} }
public CopyOnWriteArrayList<Message> getMessages( public ArrayList<Message> getMessages(Conversation conversations, int limit) {
Conversation conversations, int limit) {
return getMessages(conversations, limit, -1); return getMessages(conversations, limit, -1);
} }
public CopyOnWriteArrayList<Message> getMessages(Conversation conversation, public ArrayList<Message> getMessages(Conversation conversation, int limit,
int limit, long timestamp) { long timestamp) {
CopyOnWriteArrayList<Message> list = new CopyOnWriteArrayList<Message>(); ArrayList<Message> list = new ArrayList<Message>();
SQLiteDatabase db = this.getReadableDatabase(); SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor; Cursor cursor;
if (timestamp == -1) { if (timestamp == -1) {
@ -177,7 +177,9 @@ public class DatabaseBackend extends SQLiteOpenHelper {
if (cursor.getCount() > 0) { if (cursor.getCount() > 0) {
cursor.moveToLast(); cursor.moveToLast();
do { do {
list.add(Message.fromCursor(cursor)); Message message = Message.fromCursor(cursor);
message.setConversation(conversation);
list.add(message);
} while (cursor.moveToPrevious()); } while (cursor.moveToPrevious());
} }
return list; return list;
@ -231,10 +233,14 @@ public class DatabaseBackend extends SQLiteOpenHelper {
SQLiteDatabase db = this.getReadableDatabase(); SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from " Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from "
+ Account.TABLENAME + " where not options & (1 <<1)", null); + Account.TABLENAME + " where not options & (1 <<1)", null);
cursor.moveToFirst(); try {
int count = cursor.getInt(0); cursor.moveToFirst();
cursor.close(); int count = cursor.getInt(0);
return (count > 0); cursor.close();
return (count > 0);
} catch (SQLiteCantOpenDatabaseException e) {
return true; // better safe than sorry
}
} }
@Override @Override
@ -326,4 +332,22 @@ public class DatabaseBackend extends SQLiteOpenHelper {
cursor.moveToFirst(); cursor.moveToFirst();
return Account.fromCursor(cursor); 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.Date;
import java.util.Locale; import java.util.Locale;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Matrix; import android.graphics.Matrix;
import android.graphics.RectF; import android.graphics.RectF;
import android.media.ExifInterface;
import android.net.Uri; import android.net.Uri;
import android.os.Environment; import android.os.Environment;
import android.provider.MediaStore; import android.provider.MediaStore;
import android.util.Base64; import android.util.Base64;
import android.util.Base64OutputStream; import android.util.Base64OutputStream;
import android.util.Log; import android.util.Log;
import android.util.LruCache;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; 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.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.CryptoHelper;
import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.utils.ExifHelper;
import eu.siacs.conversations.xmpp.jingle.JingleFile;
import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.pep.Avatar;
public class FileBackend { public class FileBackend {
private static int IMAGE_SIZE = 1920; private static int IMAGE_SIZE = 1920;
private Context context;
private LruCache<String, Bitmap> thumbnailCache;
private SimpleDateFormat imageDateFormat = new SimpleDateFormat( private SimpleDateFormat imageDateFormat = new SimpleDateFormat(
"yyyyMMdd_HHmmssSSS", Locale.US); "yyyyMMdd_HHmmssSSS", Locale.US);
public FileBackend(Context context) { private XmppConnectionService mXmppConnectionService;
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;
}
};
public FileBackend(XmppConnectionService service) {
this.mXmppConnectionService = service;
} }
public LruCache<String, Bitmap> getThumbnailCache() { public DownloadableFile getFile(Message message) {
return thumbnailCache; return getFile(message, true);
} }
public JingleFile getJingleFileLegacy(Message message) { public DownloadableFile getFile(Message message, boolean decrypted) {
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) {
StringBuilder filename = new StringBuilder(); StringBuilder filename = new StringBuilder();
filename.append(Environment.getExternalStoragePublicDirectory( filename.append(getConversationsDirectory());
Environment.DIRECTORY_PICTURES).getAbsolutePath());
filename.append("/Conversations/");
filename.append(message.getUuid()); filename.append(message.getUuid());
if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) {
filename.append(".webp"); filename.append(".webp");
@ -107,7 +65,13 @@ public class FileBackend {
filename.append(".webp.pgp"); 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) { public Bitmap resize(Bitmap originalBitmap, int size) {
@ -139,17 +103,17 @@ public class FileBackend {
return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); 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 { throws ImageCopyException {
return this.copyImageToPrivateStorage(message, image, 0); return this.copyImageToPrivateStorage(message, image, 0);
} }
private JingleFile copyImageToPrivateStorage(Message message, Uri image, private DownloadableFile copyImageToPrivateStorage(Message message,
int sampleSize) throws ImageCopyException { Uri image, int sampleSize) throws ImageCopyException {
try { try {
InputStream is = context.getContentResolver() InputStream is = mXmppConnectionService.getContentResolver()
.openInputStream(image); .openInputStream(image);
JingleFile file = getJingleFile(message); DownloadableFile file = getFile(message);
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
file.createNewFile(); file.createNewFile();
Bitmap originalBitmap; Bitmap originalBitmap;
@ -202,7 +166,7 @@ public class FileBackend {
private int getRotation(Uri image) { private int getRotation(Uri image) {
if ("content".equals(image.getScheme())) { if ("content".equals(image.getScheme())) {
try { try {
Cursor cursor = context Cursor cursor = mXmppConnectionService
.getContentResolver() .getContentResolver()
.query(image, .query(image,
new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, new String[] { MediaStore.Images.ImageColumns.ORIENTATION },
@ -216,40 +180,26 @@ public class FileBackend {
return -1; return -1;
} }
} else { } else {
ExifInterface exif;
try { try {
exif = new ExifInterface(image.toString()); InputStream is = mXmppConnectionService.getContentResolver()
if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) .openInputStream(image);
.equalsIgnoreCase("6")) { return ExifHelper.getOrientation(is);
return 90; } catch (FileNotFoundException e) {
} else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) return 0;
.equalsIgnoreCase("8")) {
return 270;
} else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION)
.equalsIgnoreCase("3")) {
return 180;
} else {
return 0;
}
} catch (IOException e) {
return -1;
} }
} }
} }
public Bitmap getImageFromMessage(Message message) { public Bitmap getImageFromMessage(Message message) {
return BitmapFactory.decodeFile(getJingleFile(message) return BitmapFactory.decodeFile(getFile(message).getAbsolutePath());
.getAbsolutePath());
} }
public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) public Bitmap getThumbnail(Message message, int size, boolean cacheOnly)
throws FileNotFoundException { throws FileNotFoundException {
Bitmap thumbnail = thumbnailCache.get(message.getUuid()); Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get(
message.getUuid());
if ((thumbnail == null) && (!cacheOnly)) { if ((thumbnail == null) && (!cacheOnly)) {
File file = getJingleFile(message); File file = getFile(message);
if (!file.exists()) {
file = getJingleFileLegacy(message);
}
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(file, size); options.inSampleSize = calcSampleSize(file, size);
Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(), Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(),
@ -258,32 +208,12 @@ public class FileBackend {
throw new FileNotFoundException(); throw new FileNotFoundException();
} }
thumbnail = resize(fullsize, size); thumbnail = resize(fullsize, size);
this.thumbnailCache.put(message.getUuid(), thumbnail); this.mXmppConnectionService.getBitmapCache().put(message.getUuid(),
thumbnail);
} }
return 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() { public Uri getTakePhotoUri() {
StringBuilder pathBuilder = new StringBuilder(); StringBuilder pathBuilder = new StringBuilder();
pathBuilder.append(Environment pathBuilder.append(Environment
@ -328,7 +258,7 @@ public class FileBackend {
} }
public boolean isAvatarCached(Avatar avatar) { public boolean isAvatarCached(Avatar avatar) {
File file = new File(getAvatarPath(context, avatar.getFilename())); File file = new File(getAvatarPath(avatar.getFilename()));
return file.exists(); return file.exists();
} }
@ -336,7 +266,7 @@ public class FileBackend {
if (isAvatarCached(avatar)) { if (isAvatarCached(avatar)) {
return true; return true;
} }
String filename = getAvatarPath(context, avatar.getFilename()); String filename = getAvatarPath(avatar.getFilename());
File file = new File(filename + ".tmp"); File file = new File(filename + ".tmp");
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
try { try {
@ -368,15 +298,20 @@ public class FileBackend {
} }
} }
public static String getAvatarPath(Context context, String avatar) { public String getAvatarPath(String avatar) {
return context.getFilesDir().getAbsolutePath() + "/avatars/" + 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) { public Bitmap cropCenterSquare(Uri image, int size) {
try { try {
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = calcSampleSize(image, size); options.inSampleSize = calcSampleSize(image, size);
InputStream is = context.getContentResolver() InputStream is = mXmppConnectionService.getContentResolver()
.openInputStream(image); .openInputStream(image);
Bitmap input = BitmapFactory.decodeStream(is, null, options); Bitmap input = BitmapFactory.decodeStream(is, null, options);
if (input == null) { 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 w = input.getWidth();
int h = input.getHeight(); int h = input.getHeight();
@ -415,7 +383,7 @@ public class FileBackend {
throws FileNotFoundException { throws FileNotFoundException {
BitmapFactory.Options options = new BitmapFactory.Options(); BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; options.inJustDecodeBounds = true;
BitmapFactory.decodeStream(context.getContentResolver() BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver()
.openInputStream(image), null, options); .openInputStream(image), null, options);
return calcSampleSize(options, size); return calcSampleSize(options, size);
} }
@ -445,12 +413,8 @@ public class FileBackend {
} }
public Uri getJingleFileUri(Message message) { public Uri getJingleFileUri(Message message) {
File file = getJingleFile(message); File file = getFile(message);
if (file.exists()) { return Uri.parse("file://" + file.getAbsolutePath());
return Uri.parse("file://" + file.getAbsolutePath());
} else {
return ImageProvider.getProviderUri(message);
}
} }
public class ImageCopyException extends Exception { public class ImageCopyException extends Exception {
@ -466,12 +430,18 @@ public class FileBackend {
} }
} }
public static Bitmap getAvatar(String avatar, int size, Context context) { public Bitmap getAvatar(String avatar, int size) {
Bitmap bm = BitmapFactory.decodeFile(FileBackend.getAvatarPath(context, if (avatar == null) {
avatar)); return null;
}
Bitmap bm = cropCenter(getAvatarUri(avatar), size, size);
if (bm == null) { if (bm == null) {
return 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; package eu.siacs.conversations.services;
import java.io.FileNotFoundException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.regex.Matcher; import java.util.regex.Matcher;
@ -11,70 +12,83 @@ import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.net.Uri; import android.net.Uri;
import android.os.PowerManager; import android.os.PowerManager;
import android.os.SystemClock; import android.os.SystemClock;
import android.support.v4.app.NotificationCompat; import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationCompat.BigPictureStyle;
import android.support.v4.app.NotificationCompat.Builder;
import android.support.v4.app.TaskStackBuilder; import android.support.v4.app.TaskStackBuilder;
import android.text.Html; import android.text.Html;
import android.util.DisplayMetrics;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ConversationActivity;
public class NotificationService { public class NotificationService {
private XmppConnectionService mXmppConnectionService; private XmppConnectionService mXmppConnectionService;
private NotificationManager mNotificationManager;
private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>(); 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 Conversation mOpenConversation;
private boolean mIsInForeground; private boolean mIsInForeground;
private long mLastNotification;
private long mEndGracePeriod = 0L;
public NotificationService(XmppConnectionService service) { public NotificationService(XmppConnectionService service) {
this.mXmppConnectionService = 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 PowerManager pm = (PowerManager) mXmppConnectionService
.getSystemService(Context.POWER_SERVICE); .getSystemService(Context.POWER_SERVICE);
boolean isScreenOn = pm.isScreenOn(); boolean isScreenOn = pm.isScreenOn();
if (this.mIsInForeground && isScreenOn if (this.mIsInForeground && isScreenOn
&& this.mOpenConversation == message.getConversation()) { && this.mOpenConversation == message.getConversation()) {
return; return;
} }
String conversationUuid = message.getConversationUuid(); synchronized (notifications) {
if (notifications.containsKey(conversationUuid)) { String conversationUuid = message.getConversationUuid();
notifications.get(conversationUuid).add(message); if (notifications.containsKey(conversationUuid)) {
} else { notifications.get(conversationUuid).add(message);
ArrayList<Message> mList = new ArrayList<Message>(); } else {
mList.add(message); ArrayList<Message> mList = new ArrayList<Message>();
notifications.put(conversationUuid, mList); 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() { public void clear() {
notifications.clear(); synchronized (notifications) {
updateNotification(false); notifications.clear();
updateNotification(false);
}
} }
public void clear(Conversation conversation) { public void clear(Conversation conversation) {
notifications.remove(conversation.getUuid()); synchronized (notifications) {
updateNotification(false); notifications.remove(conversation.getUuid());
updateNotification(false);
}
} }
private void updateNotification(boolean notify) { private void updateNotification(boolean notify) {
NotificationManager notificationManager = (NotificationManager) mXmppConnectionService
.getSystemService(Context.NOTIFICATION_SERVICE);
SharedPreferences preferences = mXmppConnectionService.getPreferences(); SharedPreferences preferences = mXmppConnectionService.getPreferences();
String ringtone = preferences.getString("notification_ringtone", null); String ringtone = preferences.getString("notification_ringtone", null);
@ -82,76 +96,16 @@ public class NotificationService {
true); true);
if (notifications.size() == 0) { if (notifications.size() == 0) {
mNotificationManager.cancel(NOTIFICATION_ID); notificationManager.cancel(NOTIFICATION_ID);
} else { } else {
NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( if (notify) {
mXmppConnectionService); this.markLastNotification();
mBuilder.setSmallIcon(R.drawable.ic_notification); }
Builder mBuilder;
if (notifications.size() == 1) { if (notifications.size() == 1) {
ArrayList<Message> messages = notifications.values().iterator() mBuilder = buildSingleConversations(notify);
.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;
}
} else { } else {
NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); mBuilder = buildMultipleConversation();
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()));
}
} }
if (notify) { if (notify) {
if (vibrate) { if (vibrate) {
@ -163,12 +117,147 @@ public class NotificationService {
mBuilder.setSound(Uri.parse(ringtone)); mBuilder.setSound(Uri.parse(ringtone));
} }
} }
mBuilder.setSmallIcon(R.drawable.ic_notification);
mBuilder.setDeleteIntent(createDeleteIntent()); mBuilder.setDeleteIntent(createDeleteIntent());
if (!inGracePeriod()) { mBuilder.setLights(0xffffffff, 2000, 4000);
mBuilder.setLights(0xffffffff, 2000, 4000);
}
Notification notification = mBuilder.build(); 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; this.mIsInForeground = foreground;
} }
public void activateGracePeriod() { private int getPixel(int dp) {
this.mEndGracePeriod = SystemClock.elapsedRealtime() DisplayMetrics metrics = mXmppConnectionService.getResources()
+ (Config.CARBON_GRACE_PERIOD * 1000); .getDisplayMetrics();
return ((int) (dp * metrics.density));
} }
public void deactivateGracePeriod() { private void markLastNotification() {
this.mEndGracePeriod = 0L; this.mLastNotification = SystemClock.elapsedRealtime();
} }
private boolean inGracePeriod() { private boolean inMiniGracePeriod(Account account) {
return SystemClock.elapsedRealtime() < this.mEndGracePeriod; 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.Bookmark;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener; 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.IqGenerator;
import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.generator.MessageGenerator;
import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.generator.PresenceGenerator;
import eu.siacs.conversations.http.HttpConnectionManager;
import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.parser.IqParser;
import eu.siacs.conversations.parser.MessageParser; import eu.siacs.conversations.parser.MessageParser;
import eu.siacs.conversations.parser.PresenceParser; import eu.siacs.conversations.parser.PresenceParser;
@ -74,6 +76,7 @@ import android.net.NetworkInfo;
import android.net.Uri; import android.net.Uri;
import android.os.Binder; import android.os.Binder;
import android.os.Bundle; import android.os.Bundle;
import android.os.FileObserver;
import android.os.IBinder; import android.os.IBinder;
import android.os.PowerManager; import android.os.PowerManager;
import android.os.PowerManager.WakeLock; import android.os.PowerManager.WakeLock;
@ -81,11 +84,12 @@ import android.os.SystemClock;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.util.Log; import android.util.Log;
import android.util.LruCache;
public class XmppConnectionService extends Service { public class XmppConnectionService extends Service {
public DatabaseBackend databaseBackend; public DatabaseBackend databaseBackend;
private FileBackend fileBackend; private FileBackend fileBackend = new FileBackend(this);
public long startDate; public long startDate;
@ -94,7 +98,8 @@ public class XmppConnectionService extends Service {
private MemorizingTrustManager mMemorizingTrustManager; private MemorizingTrustManager mMemorizingTrustManager;
private NotificationService mNotificationService; private NotificationService mNotificationService = new NotificationService(
this);
private MessageParser mMessageParser = new MessageParser(this); private MessageParser mMessageParser = new MessageParser(this);
private PresenceParser mPresenceParser = new PresenceParser(this); private PresenceParser mPresenceParser = new PresenceParser(this);
@ -106,20 +111,27 @@ public class XmppConnectionService extends Service {
private CopyOnWriteArrayList<Conversation> conversations = null; private CopyOnWriteArrayList<Conversation> conversations = null;
private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager(
this); this);
private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager(
this);
private AvatarService mAvatarService = new AvatarService(this);
private OnConversationUpdate mOnConversationUpdate = null; private OnConversationUpdate mOnConversationUpdate = null;
private int convChangedListenerCount = 0; private Integer convChangedListenerCount = 0;
private OnAccountUpdate mOnAccountUpdate = null; private OnAccountUpdate mOnAccountUpdate = null;
private int accountChangedListenerCount = 0; private Integer accountChangedListenerCount = 0;
private OnRosterUpdate mOnRosterUpdate = null; private OnRosterUpdate mOnRosterUpdate = null;
private int rosterChangedListenerCount = 0; private Integer rosterChangedListenerCount = 0;
public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() {
@Override @Override
public void onContactStatusChanged(Contact contact, boolean online) { public void onContactStatusChanged(Contact contact, boolean online) {
Conversation conversation = find(getConversations(), contact); Conversation conversation = find(getConversations(), contact);
if (conversation != null) { if (conversation != null) {
conversation.endOtrIfNeeded(); if (online && contact.getPresences().size() > 1) {
conversation.endOtrIfNeeded();
} else {
conversation.resetOtrSession();
}
if (online && (contact.getPresences().size() == 1)) { if (online && (contact.getPresences().size() == 1)) {
sendUnsendMessages(conversation); 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 final IBinder mBinder = new XmppConnectionBinder();
private OnStatusChanged statusListener = new OnStatusChanged() { private OnStatusChanged statusListener = new OnStatusChanged() {
@ -252,6 +275,7 @@ public class XmppConnectionService extends Service {
} }
} }
}; };
private LruCache<String, Bitmap> mBitmapCache;
public PgpEngine getPgpEngine() { public PgpEngine getPgpEngine() {
if (pgpServiceConnection.isBound()) { if (pgpServiceConnection.isBound()) {
@ -271,6 +295,10 @@ public class XmppConnectionService extends Service {
return this.fileBackend; return this.fileBackend;
} }
public AvatarService getAvatarService() {
return this.mAvatarService;
}
public Message attachImageToConversation(final Conversation conversation, public Message attachImageToConversation(final Conversation conversation,
final Uri uri, final UiCallback<Message> callback) { final Uri uri, final UiCallback<Message> callback) {
final Message message; final Message message;
@ -331,15 +359,10 @@ public class XmppConnectionService extends Service {
} }
} }
this.wakeLock.acquire(); 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) { for (Account account : accounts) {
if (!account.isOptionSet(Account.OPTION_DISABLED)) { if (!account.isOptionSet(Account.OPTION_DISABLED)) {
if (!isConnected) { if (!hasInternetConnection()) {
account.setStatus(Account.STATUS_NO_INTERNET); account.setStatus(Account.STATUS_NO_INTERNET);
if (statusListener != null) { if (statusListener != null) {
statusListener.onStatusChanged(account); statusListener.onStatusChanged(account);
@ -398,6 +421,13 @@ public class XmppConnectionService extends Service {
return START_STICKY; 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") @SuppressLint("TrulyRandom")
@Override @Override
public void onCreate() { public void onCreate() {
@ -406,10 +436,18 @@ public class XmppConnectionService extends Service {
this.mRandom = new SecureRandom(); this.mRandom = new SecureRandom();
this.mMemorizingTrustManager = new MemorizingTrustManager( this.mMemorizingTrustManager = new MemorizingTrustManager(
getApplicationContext()); 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 this.databaseBackend = DatabaseBackend
.getInstance(getApplicationContext()); .getInstance(getApplicationContext());
this.fileBackend = new FileBackend(getApplicationContext());
this.accounts = databaseBackend.getAccounts(); this.accounts = databaseBackend.getAccounts();
for (Account account : this.accounts) { for (Account account : this.accounts) {
@ -420,6 +458,7 @@ public class XmppConnectionService extends Service {
getContentResolver().registerContentObserver( getContentResolver().registerContentObserver(
ContactsContract.Contacts.CONTENT_URI, true, contactObserver); ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
this.fileObserver.startWatching();
this.pgpServiceConnection = new OpenPgpServiceConnection( this.pgpServiceConnection = new OpenPgpServiceConnection(
getApplicationContext(), "org.sufficientlysecure.keychain"); getApplicationContext(), "org.sufficientlysecure.keychain");
this.pgpServiceConnection.bindToService(); this.pgpServiceConnection.bindToService();
@ -511,8 +550,9 @@ public class XmppConnectionService extends Service {
return connection; return connection;
} }
synchronized public void sendMessage(Message message) { public void sendMessage(Message message) {
Account account = message.getConversation().getAccount(); Account account = message.getConversation().getAccount();
account.deactivateGracePeriod();
Conversation conv = message.getConversation(); Conversation conv = message.getConversation();
MessagePacket packet = null; MessagePacket packet = null;
boolean saveInDb = true; boolean saveInDb = true;
@ -531,13 +571,14 @@ public class XmppConnectionService extends Service {
&& conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
mJingleConnectionManager mJingleConnectionManager
.createNewConnection(message); .createNewConnection(message);
} else if (message.getPresence() == null) {
message.setStatus(Message.STATUS_WAITING);
} }
} else { } else {
mJingleConnectionManager.createNewConnection(message); mJingleConnectionManager.createNewConnection(message);
} }
} else { } else {
if (message.getEncryption() == Message.ENCRYPTION_OTR) {
conv.startOtrIfNeeded();
}
message.setStatus(Message.STATUS_WAITING); message.setStatus(Message.STATUS_WAITING);
} }
} else { } else {
@ -554,6 +595,7 @@ public class XmppConnectionService extends Service {
send = true; send = true;
} else if (message.getPresence() == null) { } else if (message.getPresence() == null) {
conv.startOtrIfNeeded();
message.setStatus(Message.STATUS_WAITING); message.setStatus(Message.STATUS_WAITING);
} }
} else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { } 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 (saveInDb) {
if (message.getEncryption() == Message.ENCRYPTION_NONE if (message.getEncryption() == Message.ENCRYPTION_NONE
|| saveEncryptedMessages()) { || saveEncryptedMessages()) {
@ -776,6 +818,7 @@ public class XmppConnectionService extends Service {
.getString("photouri")); .getString("photouri"));
contact.setSystemName(phoneContact contact.setSystemName(phoneContact
.getString("displayname")); .getString("displayname"));
getAvatarService().clear(contact);
} }
} }
} }
@ -794,12 +837,39 @@ public class XmppConnectionService extends Service {
Account account = accountLookupTable.get(conv.getAccountUuid()); Account account = accountLookupTable.get(conv.getAccountUuid());
conv.setAccount(account); conv.setAccount(account);
conv.setMessages(databaseBackend.getMessages(conv, 50)); conv.setMessages(databaseBackend.getMessages(conv, 50));
checkDeletedFiles(conv);
} }
} }
return this.conversations; 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) { public void populateWithOrderedConversations(List<Conversation> list) {
populateWithOrderedConversations(list, true); populateWithOrderedConversations(list, true);
} }
@ -838,7 +908,7 @@ public class XmppConnectionService extends Service {
for (Message message : messages) { for (Message message : messages) {
message.setConversation(conversation); message.setConversation(conversation);
} }
conversation.getMessages().addAll(0, messages); conversation.addAll(0, messages);
return messages.size(); return messages.size();
} }
@ -858,9 +928,9 @@ public class XmppConnectionService extends Service {
public Conversation find(List<Conversation> haystack, Account account, public Conversation find(List<Conversation> haystack, Account account,
String jid) { String jid) {
for (Conversation conversation : haystack) { for (Conversation conversation : haystack) {
if ((account == null || conversation.getAccount().equals(account)) if ((account == null || conversation.getAccount() == account)
&& (conversation.getContactJid().split("/", 2)[0] && (conversation.getContactJid().split("/", 2)[0]
.equals(jid))) { .equalsIgnoreCase(jid))) {
return conversation; return conversation;
} }
} }
@ -927,7 +997,6 @@ public class XmppConnectionService extends Service {
public void clearConversationHistory(Conversation conversation) { public void clearConversationHistory(Conversation conversation) {
this.databaseBackend.deleteMessagesInConversation(conversation); this.databaseBackend.deleteMessagesInConversation(conversation);
this.fileBackend.removeFiles(conversation);
conversation.getMessages().clear(); conversation.getMessages().clear();
updateConversationUi(); updateConversationUi();
} }
@ -973,60 +1042,85 @@ public class XmppConnectionService extends Service {
public void setOnConversationListChangedListener( public void setOnConversationListChangedListener(
OnConversationUpdate listener) { OnConversationUpdate listener) {
this.mNotificationService.deactivateGracePeriod(); if (!isScreenOn()) {
if (checkListeners()) { Log.d(Config.LOGTAG,
switchToForeground(); "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() { public void removeOnConversationListChangedListener() {
this.convChangedListenerCount--; synchronized (this.convChangedListenerCount) {
if (this.convChangedListenerCount == 0) { this.convChangedListenerCount--;
this.mOnConversationUpdate = null; if (this.convChangedListenerCount <= 0) {
this.mNotificationService.setIsInForeground(false); this.convChangedListenerCount = 0;
if (checkListeners()) { this.mOnConversationUpdate = null;
switchToBackground(); this.mNotificationService.setIsInForeground(false);
if (checkListeners()) {
switchToBackground();
}
} }
} }
} }
public void setOnAccountListChangedListener(OnAccountUpdate listener) { public void setOnAccountListChangedListener(OnAccountUpdate listener) {
this.mNotificationService.deactivateGracePeriod(); if (!isScreenOn()) {
if (checkListeners()) { Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener");
switchToForeground(); return;
}
synchronized (this.accountChangedListenerCount) {
if (checkListeners()) {
switchToForeground();
}
this.mOnAccountUpdate = listener;
this.accountChangedListenerCount++;
} }
this.mOnAccountUpdate = listener;
this.accountChangedListenerCount++;
} }
public void removeOnAccountListChangedListener() { public void removeOnAccountListChangedListener() {
this.accountChangedListenerCount--; synchronized (this.accountChangedListenerCount) {
if (this.accountChangedListenerCount == 0) { this.accountChangedListenerCount--;
this.mOnAccountUpdate = null; if (this.accountChangedListenerCount <= 0) {
if (checkListeners()) { this.mOnAccountUpdate = null;
switchToBackground(); this.accountChangedListenerCount = 0;
if (checkListeners()) {
switchToBackground();
}
} }
} }
} }
public void setOnRosterUpdateListener(OnRosterUpdate listener) { public void setOnRosterUpdateListener(OnRosterUpdate listener) {
this.mNotificationService.deactivateGracePeriod(); if (!isScreenOn()) {
if (checkListeners()) { Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener");
switchToForeground(); return;
}
synchronized (this.rosterChangedListenerCount) {
if (checkListeners()) {
switchToForeground();
}
this.mOnRosterUpdate = listener;
this.rosterChangedListenerCount++;
} }
this.mOnRosterUpdate = listener;
this.rosterChangedListenerCount++;
} }
public void removeOnRosterUpdateListener() { public void removeOnRosterUpdateListener() {
this.rosterChangedListenerCount--; synchronized (this.rosterChangedListenerCount) {
if (this.rosterChangedListenerCount == 0) { this.rosterChangedListenerCount--;
this.mOnRosterUpdate = null; if (this.rosterChangedListenerCount <= 0) {
if (checkListeners()) { this.rosterChangedListenerCount = 0;
switchToBackground(); this.mOnRosterUpdate = null;
if (checkListeners()) {
switchToBackground();
}
} }
} }
} }
@ -1042,11 +1136,10 @@ public class XmppConnectionService extends Service {
XmppConnection connection = account.getXmppConnection(); XmppConnection connection = account.getXmppConnection();
if (connection != null && connection.getFeatures().csi()) { if (connection != null && connection.getFeatures().csi()) {
connection.sendActive(); connection.sendActive();
Log.d(Config.LOGTAG, account.getJid()
+ " sending csi//active");
} }
} }
} }
Log.d(Config.LOGTAG, "app switched into foreground");
} }
private void switchToBackground() { private void switchToBackground() {
@ -1055,11 +1148,17 @@ public class XmppConnectionService extends Service {
XmppConnection connection = account.getXmppConnection(); XmppConnection connection = account.getXmppConnection();
if (connection != null && connection.getFeatures().csi()) { if (connection != null && connection.getFeatures().csi()) {
connection.sendInactive(); 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) { public void connectMultiModeConversations(Account account) {
@ -1197,7 +1296,7 @@ public class XmppConnectionService extends Service {
conversation.getMucOptions().setOffline(); conversation.getMucOptions().setOffline();
conversation.deregisterWithBookmark(); conversation.deregisterWithBookmark();
Log.d(Config.LOGTAG, conversation.getAccount().getJid() Log.d(Config.LOGTAG, conversation.getAccount().getJid()
+ " leaving muc " + conversation.getContactJid()); + ": leaving muc " + conversation.getContactJid());
} else { } else {
account.pendingConferenceLeaves.add(conversation); account.pendingConferenceLeaves.add(conversation);
} }
@ -1214,7 +1313,11 @@ public class XmppConnectionService extends Service {
if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getMode() == Conversation.MODE_MULTI) {
leaveMuc(conversation); leaveMuc(conversation);
} else { } 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) { public void updateMessage(Message message) {
databaseBackend.updateMessage(message); databaseBackend.updateMessage(message);
updateConversationUi();
} }
protected void syncDirtyContacts(Account account) { protected void syncDirtyContacts(Account account) {
@ -1410,10 +1514,16 @@ public class XmppConnectionService extends Service {
if (account.setAvatar(avatar.getFilename())) { if (account.setAvatar(avatar.getFilename())) {
databaseBackend.updateAccount(account); databaseBackend.updateAccount(account);
} }
getAvatarService().clear(account);
updateConversationUi();
updateAccountUi();
} else { } else {
Contact contact = account.getRoster() Contact contact = account.getRoster()
.getContact(avatar.owner); .getContact(avatar.owner);
contact.setAvatar(avatar.getFilename()); contact.setAvatar(avatar.getFilename());
getAvatarService().clear(contact);
updateConversationUi();
updateRosterUi();
} }
if (callback != null) { if (callback != null) {
callback.success(avatar); callback.success(avatar);
@ -1463,6 +1573,7 @@ public class XmppConnectionService extends Service {
if (account.setAvatar(avatar.getFilename())) { if (account.setAvatar(avatar.getFilename())) {
databaseBackend.updateAccount(account); databaseBackend.updateAccount(account);
} }
getAvatarService().clear(account);
callback.success(avatar); callback.success(avatar);
} else { } else {
fetchAvatar(account, avatar, callback); fetchAvatar(account, avatar, callback);
@ -1675,6 +1786,10 @@ public class XmppConnectionService extends Service {
return this.pm; return this.pm;
} }
public LruCache<String, Bitmap> getBitmapCache() {
return this.mBitmapCache;
}
public void replyWithNotAcceptable(Account account, MessagePacket packet) { public void replyWithNotAcceptable(Account account, MessagePacket packet) {
if (account.getStatus() == Account.STATUS_ONLINE) { if (account.getStatus() == Account.STATUS_ONLINE) {
MessagePacket error = this.mMessageGenerator MessagePacket error = this.mMessageGenerator
@ -1779,8 +1894,7 @@ public class XmppConnectionService extends Service {
ArrayList<Contact> contacts = new ArrayList<Contact>(); ArrayList<Contact> contacts = new ArrayList<Contact>();
for (Account account : getAccounts()) { for (Account account : getAccounts()) {
if (!account.isOptionSet(Account.OPTION_DISABLED)) { if (!account.isOptionSet(Account.OPTION_DISABLED)) {
Contact contact = account.getRoster() Contact contact = account.getRoster().getContactFromRoster(jid);
.getContactAsShownInRoster(jid);
if (contact != null) { if (contact != null) {
contacts.add(contact); contacts.add(contact);
} }
@ -1792,4 +1906,44 @@ public class XmppConnectionService extends Service {
public NotificationService getNotificationService() { public NotificationService getNotificationService() {
return this.mNotificationService; 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 @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.choose_contact, 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(); View mSearchView = menuSearchView.getActionView();
mSearchEditText = (EditText) mSearchView mSearchEditText = (EditText) mSearchView
.findViewById(R.id.search_field); .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.R;
import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.PgpEngine;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions;
import eu.siacs.conversations.entities.MucOptions.OnRenameListener; import eu.siacs.conversations.entities.MucOptions.OnRenameListener;
import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate;
import eu.siacs.conversations.utils.UIHelper;
import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.Context; import android.content.Context;
@ -41,6 +39,7 @@ public class ConferenceDetailsActivity extends XmppActivity {
private ImageButton mEditNickButton; private ImageButton mEditNickButton;
private TextView mRoleAffiliaton; private TextView mRoleAffiliaton;
private TextView mFullJid; private TextView mFullJid;
private TextView mAccountJid;
private LinearLayout membersView; private LinearLayout membersView;
private LinearLayout mMoreDetails; private LinearLayout mMoreDetails;
private Button mInviteButton; private Button mInviteButton;
@ -78,6 +77,7 @@ public class ConferenceDetailsActivity extends XmppActivity {
mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button); mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button);
mFullJid = (TextView) findViewById(R.id.muc_jabberid); mFullJid = (TextView) findViewById(R.id.muc_jabberid);
membersView = (LinearLayout) findViewById(R.id.muc_members); membersView = (LinearLayout) findViewById(R.id.muc_members);
mAccountJid = (TextView) findViewById(R.id.details_account);
mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details); mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details);
mMoreDetails.setVisibility(View.GONE); mMoreDetails.setVisibility(View.GONE);
mInviteButton = (Button) findViewById(R.id.invite); mInviteButton = (Button) findViewById(R.id.invite);
@ -169,37 +169,38 @@ public class ConferenceDetailsActivity extends XmppActivity {
} }
protected void registerListener() { protected void registerListener() {
if (xmppConnectionServiceBound) { xmppConnectionService
xmppConnectionService .setOnConversationListChangedListener(this.onConvChanged);
.setOnConversationListChangedListener(this.onConvChanged); xmppConnectionService.setOnRenameListener(new OnRenameListener() {
xmppConnectionService.setOnRenameListener(new OnRenameListener() {
@Override @Override
public void onRename(final boolean success) { public void onRename(final boolean success) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
populateView(); populateView();
if (success) { if (success) {
Toast.makeText( Toast.makeText(
ConferenceDetailsActivity.this, ConferenceDetailsActivity.this,
getString(R.string.your_nick_has_been_changed), getString(R.string.your_nick_has_been_changed),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
} else { } else {
Toast.makeText(ConferenceDetailsActivity.this, Toast.makeText(ConferenceDetailsActivity.this,
getString(R.string.nick_in_use), getString(R.string.nick_in_use),
Toast.LENGTH_SHORT).show(); Toast.LENGTH_SHORT).show();
}
} }
}); }
} });
}); }
} });
} }
private void populateView() { 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()); setTitle(conversation.getName());
mFullJid.setText(conversation.getContactJid().split("/", 2)[0]); mFullJid.setText(conversation.getContactJid().split("/", 2)[0]);
mYourNick.setText(conversation.getMucOptions().getActualNick()); mYourNick.setText(conversation.getMucOptions().getActualNick());
@ -225,9 +226,8 @@ public class ConferenceDetailsActivity extends XmppActivity {
this.users.addAll(conversation.getMucOptions().getUsers()); this.users.addAll(conversation.getMucOptions().getUsers());
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
membersView.removeAllViews(); membersView.removeAllViews();
Account account = conversation.getAccount();
for (final User user : conversation.getMucOptions().getUsers()) { 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); false);
TextView name = (TextView) view TextView name = (TextView) view
.findViewById(R.id.contact_display_name); .findViewById(R.id.contact_display_name);
@ -245,22 +245,14 @@ public class ConferenceDetailsActivity extends XmppActivity {
key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId()));
} }
Bitmap bm; Bitmap bm;
if (user.getJid() != null) { Contact contact = user.getContact();
Contact contact = account.getRoster().getContact(user.getJid()); if (contact != null) {
if (contact.showInRoster()) { bm = avatarService().get(contact, getPixel(48));
bm = contact.getImage(48, this); name.setText(contact.getDisplayName());
name.setText(contact.getDisplayName()); role.setText(user.getName() + " \u2022 "
role.setText(user.getName() + " \u2022 " + getReadableRole(user.getRole()));
+ getReadableRole(user.getRole()));
} else {
bm = UIHelper.getContactPicture(user.getName(), 48, this,
false);
name.setText(user.getName());
role.setText(getReadableRole(user.getRole()));
}
} else { } else {
bm = UIHelper bm = avatarService().get(user.getName(), getPixel(48));
.getContactPicture(user.getName(), 48, this, false);
name.setText(user.getName()); name.setText(user.getName());
role.setText(getReadableRole(user.getRole())); role.setText(getReadableRole(user.getRole()));
} }

View file

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

View file

@ -191,6 +191,7 @@ public class ConversationActivity extends XmppActivity implements
xmppConnectionService.getNotificationService() xmppConnectionService.getNotificationService()
.setOpenConversation(null); .setOpenConversation(null);
} }
closeContextMenu();
} }
@Override @Override
@ -222,7 +223,8 @@ public class ConversationActivity extends XmppActivity implements
ab.setDisplayHomeAsUpEnabled(true); ab.setDisplayHomeAsUpEnabled(true);
ab.setHomeButtonEnabled(true); ab.setHomeButtonEnabled(true);
if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE
|| ConversationActivity.this.useSubjectToIdentifyConference()) { || ConversationActivity.this
.useSubjectToIdentifyConference()) {
ab.setTitle(getSelectedConversation().getName()); ab.setTitle(getSelectedConversation().getName());
} else { } else {
ab.setTitle(getSelectedConversation().getContactJid() ab.setTitle(getSelectedConversation().getContactJid()
@ -239,19 +241,16 @@ public class ConversationActivity extends XmppActivity implements
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.conversations, menu); getMenuInflater().inflate(R.menu.conversations, menu);
MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security); MenuItem menuSecure = menu.findItem(R.id.action_security);
MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive); MenuItem menuArchive = menu.findItem(R.id.action_archive);
MenuItem menuMucDetails = (MenuItem) menu MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details);
.findItem(R.id.action_muc_details); MenuItem menuContactDetails = menu
MenuItem menuContactDetails = (MenuItem) menu
.findItem(R.id.action_contact_details); .findItem(R.id.action_contact_details);
MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file); MenuItem menuAttach = menu.findItem(R.id.action_attach_file);
MenuItem menuClearHistory = (MenuItem) menu MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history);
.findItem(R.id.action_clear_history); MenuItem menuAdd = menu.findItem(R.id.action_add);
MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add); MenuItem menuInviteContact = menu.findItem(R.id.action_invite);
MenuItem menuInviteContact = (MenuItem) menu MenuItem menuMute = menu.findItem(R.id.action_mute);
.findItem(R.id.action_invite);
MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute);
if (isConversationsOverviewVisable() if (isConversationsOverviewVisable()
&& isConversationsOverviewHideable()) { && isConversationsOverviewHideable()) {
@ -604,8 +603,11 @@ public class ConversationActivity extends XmppActivity implements
.beginTransaction(); .beginTransaction();
transaction.replace(R.id.selected_conversation, selectedFragment, transaction.replace(R.id.selected_conversation, selectedFragment,
"conversation"); "conversation");
try {
transaction.commitAllowingStateLoss(); transaction.commitAllowingStateLoss();
} catch (IllegalStateException e) {
return selectedFragment;
}
} }
return selectedFragment; return selectedFragment;
} }
@ -624,23 +626,10 @@ public class ConversationActivity extends XmppActivity implements
@Override @Override
protected void onNewIntent(Intent intent) { protected void onNewIntent(Intent intent) {
if (xmppConnectionServiceBound) { if (xmppConnectionServiceBound) {
if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) {
.equals(intent.getType())))) { handleViewConversationIntent(intent);
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);
} }
} else { } else {
handledViewIntent = false;
setIntent(intent); setIntent(intent);
} }
} }
@ -690,6 +679,10 @@ public class ConversationActivity extends XmppActivity implements
} else if (conversationList.size() <= 0) { } else if (conversationList.size() <= 0) {
startActivity(new Intent(this, StartConversationActivity.class)); startActivity(new Intent(this, StartConversationActivity.class));
finish(); finish();
} else if (getIntent() != null
&& VIEW_CONVERSATION.equals(getIntent().getType())) {
handleViewConversationIntent(getIntent());
setIntent(null);
} else if (mOpenConverstaion != null) { } else if (mOpenConverstaion != null) {
selectConversationByUuid(mOpenConverstaion); selectConversationByUuid(mOpenConverstaion);
paneShouldBeOpen = mPanelOpen; paneShouldBeOpen = mPanelOpen;
@ -698,14 +691,6 @@ public class ConversationActivity extends XmppActivity implements
} }
swapConversationFragment(); swapConversationFragment();
mOpenConverstaion = null; 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 { } else {
showConversationsOverview(); showConversationsOverview();
ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager()
@ -727,6 +712,14 @@ public class ConversationActivity extends XmppActivity implements
ExceptionHelper.checkForCrash(this, this.xmppConnectionService); 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) { private void selectConversationByUuid(String uuid) {
for (int i = 0; i < conversationList.size(); ++i) { for (int i = 0; i < conversationList.size(); ++i) {
if (conversationList.get(i).getUuid().equals(uuid)) { if (conversationList.get(i).getUuid().equals(uuid)) {
@ -736,11 +729,9 @@ public class ConversationActivity extends XmppActivity implements
} }
public void registerListener() { public void registerListener() {
if (xmppConnectionServiceBound) { xmppConnectionService.setOnConversationListChangedListener(this);
xmppConnectionService.setOnConversationListChangedListener(this); xmppConnectionService.setOnAccountListChangedListener(this);
xmppConnectionService.setOnAccountListChangedListener(this); xmppConnectionService.setOnRosterUpdateListener(this);
xmppConnectionService.setOnRosterUpdateListener(this);
}
} }
@Override @Override
@ -753,6 +744,7 @@ public class ConversationActivity extends XmppActivity implements
.findFragmentByTag("conversation"); .findFragmentByTag("conversation");
if (selectedFragment != null) { if (selectedFragment != null) {
selectedFragment.hideSnackbar(); selectedFragment.hideSnackbar();
selectedFragment.updateMessages();
} }
} else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) {
pendingImageUri = data.getData(); pendingImageUri = data.getData();
@ -786,6 +778,10 @@ public class ConversationActivity extends XmppActivity implements
attachAudioToConversation(getSelectedConversation(), attachAudioToConversation(getSelectedConversation(),
data.getData()); 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.ArrayList;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import net.java.otr4j.session.SessionStatus; import net.java.otr4j.session.SessionStatus;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
@ -32,9 +33,12 @@ import android.content.IntentSender.SendIntentException;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
import android.text.Selection; import android.text.Selection;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.Gravity; import android.view.Gravity;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -44,6 +48,8 @@ import android.widget.AbsListView.OnScrollListener;
import android.widget.TextView.OnEditorActionListener; import android.widget.TextView.OnEditorActionListener;
import android.widget.AbsListView; import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.ListView; import android.widget.ListView;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
@ -72,6 +78,9 @@ public class ConversationFragment extends Fragment {
private IntentSender askForPassphraseIntent = null; private IntentSender askForPassphraseIntent = null;
private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<Message>();
private boolean mDecryptJobRunning = false;
private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() { private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() {
@Override @Override
@ -189,6 +198,7 @@ public class ConversationFragment extends Fragment {
}; };
private ConversationActivity activity; private ConversationActivity activity;
private Message selectedMessage;
private void sendMessage() { private void sendMessage() {
if (this.conversation == null) { if (this.conversation == null) {
@ -322,9 +332,114 @@ public class ConversationFragment extends Fragment {
}); });
messagesView.setAdapter(messageListAdapter); messagesView.setAdapter(messageListAdapter);
registerForContextMenu(messagesView);
return view; 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) { protected void privateMessageWith(String counterpart) {
this.mEditMessage.setText(""); this.mEditMessage.setText("");
this.conversation.setNextPresence(counterpart); this.conversation.setNextPresence(counterpart);
@ -356,6 +471,7 @@ public class ConversationFragment extends Fragment {
@Override @Override
public void onStop() { public void onStop() {
mDecryptJobRunning = false;
super.onStop(); super.onStop();
if (this.conversation != null) { if (this.conversation != null) {
this.conversation.setNextMessage(mEditMessage.getText().toString()); this.conversation.setNextMessage(mEditMessage.getText().toString());
@ -395,34 +511,6 @@ public class ConversationFragment extends Fragment {
updateMessages(); 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() { public void updateMessages() {
if (getView() == null) { if (getView() == null) {
return; return;
@ -458,13 +546,16 @@ public class ConversationFragment extends Fragment {
}); });
} }
for (Message message : this.conversation.getMessages()) { for (Message message : this.conversation.getMessages()) {
if ((message.getEncryption() == Message.ENCRYPTION_PGP) if (message.getEncryption() == Message.ENCRYPTION_PGP
&& ((message.getStatus() == Message.STATUS_RECEIVED) || (message && (message.getStatus() == Message.STATUS_RECEIVED || message
.getStatus() == Message.STATUS_SEND))) { .getStatus() >= Message.STATUS_SEND)
decryptMessage(message); && message.getDownloadable() == null) {
break; if (!mEncryptedMessages.contains(message)) {
mEncryptedMessages.add(message);
}
} }
} }
decryptNext();
this.messageList.clear(); this.messageList.clear();
if (this.conversation.getMessages().size() == 0) { if (this.conversation.getMessages().size() == 0) {
messagesLoaded = false; messagesLoaded = false;
@ -476,7 +567,7 @@ public class ConversationFragment extends Fragment {
this.messageListAdapter.notifyDataSetChanged(); this.messageListAdapter.notifyDataSetChanged();
if (conversation.getMode() == Conversation.MODE_SINGLE) { if (conversation.getMode() == Conversation.MODE_SINGLE) {
if (messageList.size() >= 1) { if (messageList.size() >= 1) {
makeFingerprintWarning(conversation.getLatestEncryption()); makeFingerprintWarning();
} }
} else { } else {
if (!conversation.getMucOptions().online() 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() { private void messageSent() {
int size = this.messageList.size(); int size = this.messageList.size();
messagesView.setSelection(size - 1); 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() Set<String> knownFingerprints = conversation.getContact()
.getOtrFingerprints(); .getOtrFingerprints();
if ((latestEncryption == Message.ENCRYPTION_OTR) if (conversation.hasValidOtrSession()
&& (conversation.hasValidOtrSession()
&& (!conversation.isMuted()) && (!conversation.isMuted())
&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
.contains(conversation.getOtrFingerprint())))) { .contains(conversation.getOtrFingerprint()))) {
showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify,
new OnClickListener() { new OnClickListener() {

View file

@ -1,8 +1,6 @@
package eu.siacs.conversations.ui; package eu.siacs.conversations.ui;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.text.Editable; import android.text.Editable;
@ -17,6 +15,7 @@ import android.widget.EditText;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
@ -43,7 +42,7 @@ public class EditAccountActivity extends XmppActivity {
private TextView mServerInfoPep; private TextView mServerInfoPep;
private TextView mSessionEst; private TextView mSessionEst;
private TextView mOtrFingerprint; private TextView mOtrFingerprint;
private TextView mOtrFingerprintHeadline; private RelativeLayout mOtrFingerprintBox;
private ImageButton mOtrFingerprintToClipboardButton; private ImageButton mOtrFingerprintToClipboardButton;
private String jidToEdit; private String jidToEdit;
@ -277,7 +276,7 @@ public class EditAccountActivity extends XmppActivity {
this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm);
this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep);
this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); 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.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard);
this.mSaveButton = (Button) findViewById(R.id.save_button); this.mSaveButton = (Button) findViewById(R.id.save_button);
this.mCancelButton = (Button) findViewById(R.id.cancel_button); this.mCancelButton = (Button) findViewById(R.id.cancel_button);
@ -378,8 +377,7 @@ public class EditAccountActivity extends XmppActivity {
final String fingerprint = this.mAccount final String fingerprint = this.mAccount
.getOtrFingerprint(xmppConnectionService); .getOtrFingerprint(xmppConnectionService);
if (fingerprint != null) { if (fingerprint != null) {
this.mOtrFingerprintHeadline.setVisibility(View.VISIBLE); this.mOtrFingerprintBox.setVisibility(View.VISIBLE);
this.mOtrFingerprint.setVisibility(View.VISIBLE);
this.mOtrFingerprint.setText(fingerprint); this.mOtrFingerprint.setText(fingerprint);
this.mOtrFingerprintToClipboardButton this.mOtrFingerprintToClipboardButton
.setVisibility(View.VISIBLE); .setVisibility(View.VISIBLE);
@ -389,7 +387,7 @@ public class EditAccountActivity extends XmppActivity {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (OtrFingerprintToClipBoard(fingerprint)) { if (copyTextToClipboard(fingerprint,R.string.otr_fingerprint)) {
Toast.makeText( Toast.makeText(
EditAccountActivity.this, EditAccountActivity.this,
R.string.toast_message_otr_fingerprint, R.string.toast_message_otr_fingerprint,
@ -398,9 +396,7 @@ public class EditAccountActivity extends XmppActivity {
} }
}); });
} else { } else {
this.mOtrFingerprintToClipboardButton.setVisibility(View.GONE); this.mOtrFingerprintBox.setVisibility(View.GONE);
this.mOtrFingerprint.setVisibility(View.GONE);
this.mOtrFingerprintHeadline.setVisibility(View.GONE);
} }
} else { } else {
if (this.mAccount.errorStatus()) { if (this.mAccount.errorStatus()) {
@ -411,15 +407,4 @@ public class EditAccountActivity extends XmppActivity {
this.mStats.setVisibility(View.GONE); 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, public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) { ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, 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; AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo;
this.selectedAccount = accountList.get(acmi.position); this.selectedAccount = accountList.get(acmi.position);
if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) { if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) {
@ -122,6 +123,7 @@ public class ManageAccountActivity extends XmppActivity {
return true; return true;
case R.id.mgmt_account_announce_pgp: case R.id.mgmt_account_announce_pgp:
publishOpenPGPPublicKey(selectedAccount); publishOpenPGPPublicKey(selectedAccount);
return true;
default: default:
return super.onContextItemSelected(item); return super.onContextItemSelected(item);
} }
@ -187,7 +189,8 @@ public class ManageAccountActivity extends XmppActivity {
} }
private void deleteAccount(final Account account) { 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.setTitle(getString(R.string.mgmt_account_are_you_sure));
builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); 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.avatarUri == null) {
if (this.account.getAvatar() != null if (this.account.getAvatar() != null
|| this.defaultUri == null) { || this.defaultUri == null) {
this.avatar.setImageBitmap(this.account.getImage( this.avatar.setImageBitmap(avatarService().get(account,
getApplicationContext(), 384)); getPixel(194)));
if (this.defaultUri != null) { if (this.defaultUri != null) {
this.avatar this.avatar
.setOnLongClickListener(this.backToDefaultListener); .setOnLongClickListener(this.backToDefaultListener);

View file

@ -463,11 +463,11 @@ public class StartConversationActivity extends XmppActivity {
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
this.mOptionsMenu = menu; this.mOptionsMenu = menu;
getMenuInflater().inflate(R.menu.start_conversation, menu); getMenuInflater().inflate(R.menu.start_conversation, menu);
MenuItem menuCreateContact = (MenuItem) menu MenuItem menuCreateContact = menu
.findItem(R.id.action_create_contact); .findItem(R.id.action_create_contact);
MenuItem menuCreateConference = (MenuItem) menu MenuItem menuCreateConference = menu
.findItem(R.id.action_join_conference); .findItem(R.id.action_join_conference);
mMenuSearchView = (MenuItem) menu.findItem(R.id.action_search); mMenuSearchView = menu.findItem(R.id.action_search);
mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener); mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener);
View mSearchView = mMenuSearchView.getActionView(); View mSearchView = mMenuSearchView.getActionView();
mSearchEditText = (EditText) mSearchView 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.Conversation;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.Presences;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.ExceptionHelper;
@ -20,6 +21,8 @@ import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.AlertDialog.Builder; import android.app.AlertDialog.Builder;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
@ -55,7 +58,6 @@ public abstract class XmppActivity extends Activity {
public XmppConnectionService xmppConnectionService; public XmppConnectionService xmppConnectionService;
public boolean xmppConnectionServiceBound = false; public boolean xmppConnectionServiceBound = false;
protected boolean handledViewIntent = false;
protected int mPrimaryTextColor; protected int mPrimaryTextColor;
protected int mSecondaryTextColor; protected int mSecondaryTextColor;
@ -400,8 +402,7 @@ public abstract class XmppActivity extends Activity {
private void quickEdit(final String previousValue, private void quickEdit(final String previousValue,
final OnValueEdited callback, boolean password) { final OnValueEdited callback, boolean password) {
AlertDialog.Builder builder = new AlertDialog.Builder(this); AlertDialog.Builder builder = new AlertDialog.Builder(this);
View view = (View) getLayoutInflater() View view = getLayoutInflater().inflate(R.layout.quickedit, null);
.inflate(R.layout.quickedit, null);
final EditText editor = (EditText) view.findViewById(R.id.editor); final EditText editor = (EditText) view.findViewById(R.id.editor);
OnClickListener mClickListener = new OnClickListener() { OnClickListener mClickListener = new OnClickListener() {
@ -448,7 +449,7 @@ public abstract class XmppActivity extends Activity {
listener.onPresenceSelected(); listener.onPresenceSelected();
} }
} else if (presences.size() == 1) { } else if (presences.size() == 1) {
String presence = (String) presences.asStringArray()[0]; String presence = presences.asStringArray()[0];
conversation.setNextPresence(presence); conversation.setNextPresence(presence);
listener.onPresenceSelected(); listener.onPresenceSelected();
} else { } else {
@ -526,6 +527,26 @@ public abstract class XmppActivity extends Activity {
return this.mSecondaryBackgroundColor; 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> { class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference; private final WeakReference<ImageView> imageViewReference;
private Message message = null; private Message message = null;

View file

@ -28,13 +28,14 @@ public class AccountAdapter extends ArrayAdapter<Account> {
if (view == null) { if (view == null) {
LayoutInflater inflater = (LayoutInflater) getContext() LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE); .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); TextView jid = (TextView) view.findViewById(R.id.account_jid);
jid.setText(account.getJid()); jid.setText(account.getJid());
TextView statusView = (TextView) view.findViewById(R.id.account_status); TextView statusView = (TextView) view.findViewById(R.id.account_status);
ImageView imageView = (ImageView) view.findViewById(R.id.account_image); 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()) { switch (account.getStatus()) {
case Account.STATUS_DISABLED: case Account.STATUS_DISABLED:
statusView.setText(getContext().getString( statusView.setText(getContext().getString(

View file

@ -5,6 +5,7 @@ import java.util.List;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.XmppActivity;
@ -34,14 +35,14 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
if (view == null) { if (view == null) {
LayoutInflater inflater = (LayoutInflater) activity LayoutInflater inflater = (LayoutInflater) activity
.getSystemService(Context.LAYOUT_INFLATER_SERVICE); .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
view = (View) inflater.inflate(R.layout.conversation_list_row, view = inflater.inflate(R.layout.conversation_list_row,
parent, false); parent, false);
} }
Conversation conv = getItem(position); Conversation conversation = getItem(position);
if (this.activity instanceof ConversationActivity) { if (this.activity instanceof ConversationActivity) {
ConversationActivity activity = (ConversationActivity) this.activity; ConversationActivity activity = (ConversationActivity) this.activity;
if (!activity.isConversationsOverviewHideable()) { if (!activity.isConversationsOverviewHideable()) {
if (conv == activity.getSelectedConversation()) { if (conversation == activity.getSelectedConversation()) {
view.setBackgroundColor(activity view.setBackgroundColor(activity
.getSecondaryBackgroundColor()); .getSecondaryBackgroundColor());
} else { } else {
@ -53,65 +54,85 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> {
} }
TextView convName = (TextView) view TextView convName = (TextView) view
.findViewById(R.id.conversation_name); .findViewById(R.id.conversation_name);
if (conv.getMode() == Conversation.MODE_SINGLE if (conversation.getMode() == Conversation.MODE_SINGLE
|| activity.useSubjectToIdentifyConference()) { || activity.useSubjectToIdentifyConference()) {
convName.setText(conv.getName()); convName.setText(conversation.getName());
} else { } 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); .findViewById(R.id.conversation_lastmsg);
TextView mTimestamp = (TextView) view
.findViewById(R.id.conversation_lastupdate);
ImageView imagePreview = (ImageView) view ImageView imagePreview = (ImageView) view
.findViewById(R.id.conversation_lastimage); .findViewById(R.id.conversation_lastimage);
Message latestMessage = conv.getLatestMessage(); Message message = conversation.getLatestMessage();
if (latestMessage.getType() == Message.TYPE_TEXT if (!conversation.isRead()) {
|| 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()) {
convName.setTypeface(null, Typeface.BOLD); convName.setTypeface(null, Typeface.BOLD);
convLastMsg.setTypeface(null, Typeface.BOLD);
} else { } else {
convName.setTypeface(null, Typeface.NORMAL); convName.setTypeface(null, Typeface.NORMAL);
convLastMsg.setTypeface(null, Typeface.NORMAL);
} }
((TextView) view.findViewById(R.id.conversation_lastupdate)) if (message.getType() == Message.TYPE_IMAGE
.setText(UIHelper.readableTimeDifference(getContext(), conv || message.getDownloadable() != null) {
.getLatestMessage().getTimeSent())); 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 ImageView profilePicture = (ImageView) view
.findViewById(R.id.conversation_image); .findViewById(R.id.conversation_image);
profilePicture.setImageBitmap(conv.getImage(activity, 56)); profilePicture.setImageBitmap(activity.avatarService().get(
conversation, activity.getPixel(56)));
return view; return view;
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -33,7 +33,7 @@ public class DNSHelper {
for (String dnsserver : dns) { for (String dnsserver : dns) {
InetAddress ip = InetAddress.getByName(dnsserver); InetAddress ip = InetAddress.getByName(dnsserver);
Bundle b = queryDNS(host, ip); Bundle b = queryDNS(host, ip);
if (b.containsKey("name")) { if (b.containsKey("values")) {
return b; return b;
} else if (b.containsKey("error") } else if (b.containsKey("error")
&& "nosrv".equals(b.getString("error", null))) { && "nosrv".equals(b.getString("error", null))) {
@ -45,7 +45,7 @@ public class DNSHelper {
} }
public static Bundle queryDNS(String host, InetAddress dnsServer) { public static Bundle queryDNS(String host, InetAddress dnsServer) {
Bundle namePort = new Bundle(); Bundle bundle = new Bundle();
try { try {
String qname = "_xmpp-client._tcp." + host; String qname = "_xmpp-client._tcp." + host;
Log.d(Config.LOGTAG, Log.d(Config.LOGTAG,
@ -133,42 +133,28 @@ public class DNSHelper {
} }
if (result.size() == 0) { if (result.size() == 0) {
namePort.putString("error", "nosrv"); bundle.putString("error", "nosrv");
return namePort; return bundle;
} }
// we now have a list of servers to try :-) ArrayList<Bundle> values = new ArrayList<Bundle>();
// 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;
for (SRV srv : result) { for (SRV srv : result) {
namePort.putString("name" + i, srv.getName()); Bundle namePort = new Bundle();
namePort.putInt("port" + i, srv.getPort()); namePort.putString("name", srv.getName());
i++; 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) { } catch (SocketTimeoutException e) {
namePort.putString("error", "timeout"); bundle.putString("error", "timeout");
} catch (Exception e) { } catch (Exception e) {
namePort.putString("error", "unhandled"); bundle.putString("error", "unhandled");
} }
return namePort; return bundle;
} }
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); 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, final String[] PROJECTION = new String[] { ContactsContract.Data._ID,
ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.DISPLAY_NAME,
ContactsContract.Data.PHOTO_THUMBNAIL_URI, ContactsContract.Data.PHOTO_URI,
ContactsContract.Data.LOOKUP_KEY, ContactsContract.Data.LOOKUP_KEY,
ContactsContract.CommonDataKinds.Im.DATA }; ContactsContract.CommonDataKinds.Im.DATA };
@ -50,10 +50,8 @@ public class PhoneHelper {
"displayname", "displayname",
cursor.getString(cursor cursor.getString(cursor
.getColumnIndex(ContactsContract.Data.DISPLAY_NAME))); .getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
contact.putString( contact.putString("photouri", cursor.getString(cursor
"photouri", .getColumnIndex(ContactsContract.Data.PHOTO_URI)));
cursor.getString(cursor
.getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI)));
contact.putString("lookup", cursor.getString(cursor contact.putString("lookup", cursor.getString(cursor
.getColumnIndex(ContactsContract.Data.LOOKUP_KEY))); .getColumnIndex(ContactsContract.Data.LOOKUP_KEY)));

View file

@ -1,22 +1,18 @@
package eu.siacs.conversations.utils; package eu.siacs.conversations.utils;
import java.io.FileNotFoundException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.MucOptions.User;
import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ConversationActivity;
import eu.siacs.conversations.ui.ManageAccountActivity; import eu.siacs.conversations.ui.ManageAccountActivity;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager; import android.app.NotificationManager;
@ -25,28 +21,15 @@ import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.Intent; 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.NotificationCompat;
import android.support.v4.app.TaskStackBuilder; import android.support.v4.app.TaskStackBuilder;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.util.DisplayMetrics;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.QuickContactBadge;
import android.widget.TextView; import android.widget.TextView;
public class UIHelper { 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 private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE
| DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL;
private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME 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, public static void showErrorNotification(Context context,
List<Account> accounts) { List<Account> accounts) {
NotificationManager mNotificationManager = (NotificationManager) context NotificationManager mNotificationManager = (NotificationManager) context
@ -326,16 +148,6 @@ public class UIHelper {
mNotificationManager.notify(1111, notification); 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") @SuppressLint("InflateParams")
public static AlertDialog getVerifyFingerprintDialog( public static AlertDialog getVerifyFingerprintDialog(
final ConversationActivity activity, final ConversationActivity activity,
@ -370,25 +182,6 @@ public class UIHelper {
return builder.create(); 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 { private final static class EmoticonPattern {
Pattern pattern; Pattern pattern;
String replacement; String replacement;

View file

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

View file

@ -16,6 +16,7 @@ import eu.siacs.conversations.Config;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Downloadable; import eu.siacs.conversations.entities.Downloadable;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
@ -33,17 +34,18 @@ public class JingleConnection implements Downloadable {
private JingleConnectionManager mJingleConnectionManager; private JingleConnectionManager mJingleConnectionManager;
private XmppConnectionService mXmppConnectionService; private XmppConnectionService mXmppConnectionService;
public static final int STATUS_INITIATED = 0; protected static final int JINGLE_STATUS_INITIATED = 0;
public static final int STATUS_ACCEPTED = 1; protected static final int JINGLE_STATUS_ACCEPTED = 1;
public static final int STATUS_TERMINATED = 2; protected static final int JINGLE_STATUS_TERMINATED = 2;
public static final int STATUS_CANCELED = 3; protected static final int JINGLE_STATUS_CANCELED = 3;
public static final int STATUS_FINISHED = 4; protected static final int JINGLE_STATUS_FINISHED = 4;
public static final int STATUS_TRANSMITTING = 5; protected static final int JINGLE_STATUS_TRANSMITTING = 5;
public static final int STATUS_FAILED = 99; protected static final int JINGLE_STATUS_FAILED = 99;
private int ibbBlockSize = 4096; private int ibbBlockSize = 4096;
private int status = -1; private int mJingleStatus = -1;
private int mStatus = -1;
private Message message; private Message message;
private String sessionId; private String sessionId;
private Account account; private Account account;
@ -54,7 +56,7 @@ public class JingleConnection implements Downloadable {
private String transportId; private String transportId;
private Element fileOffer; private Element fileOffer;
private JingleFile file = null; private DownloadableFile file = null;
private String contentName; private String contentName;
private String contentCreator; private String contentCreator;
@ -71,11 +73,7 @@ public class JingleConnection implements Downloadable {
@Override @Override
public void onIqPacketReceived(Account account, IqPacket packet) { public void onIqPacketReceived(Account account, IqPacket packet) {
if (packet.getType() == IqPacket.TYPE_ERROR) { if (packet.getType() == IqPacket.TYPE_ERROR) {
if (initiator.equals(account.getFullJid())) { cancel();
mXmppConnectionService.markMessage(message,
Message.STATUS_SEND_FAILED);
}
status = STATUS_FAILED;
} }
} }
}; };
@ -83,7 +81,7 @@ public class JingleConnection implements Downloadable {
final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() {
@Override @Override
public void onFileTransmitted(JingleFile file) { public void onFileTransmitted(DownloadableFile file) {
if (responder.equals(account.getFullJid())) { if (responder.equals(account.getFullJid())) {
sendSuccess(); sendSuccess();
if (acceptedAutomatically) { if (acceptedAutomatically) {
@ -96,8 +94,8 @@ public class JingleConnection implements Downloadable {
BitmapFactory.decodeFile(file.getAbsolutePath(), options); BitmapFactory.decodeFile(file.getAbsolutePath(), options);
int imageHeight = options.outHeight; int imageHeight = options.outHeight;
int imageWidth = options.outWidth; int imageWidth = options.outWidth;
message.setBody(Long.toString(file.getSize()) + ',' message.setBody(Long.toString(file.getSize()) + '|'
+ imageWidth + ',' + imageHeight); + imageWidth + '|' + imageHeight);
mXmppConnectionService.databaseBackend.createMessage(message); mXmppConnectionService.databaseBackend.createMessage(message);
mXmppConnectionService.markMessage(message, mXmppConnectionService.markMessage(message,
Message.STATUS_RECEIVED); Message.STATUS_RECEIVED);
@ -148,8 +146,8 @@ public class JingleConnection implements Downloadable {
return this.sessionId; return this.sessionId;
} }
public String getAccountJid() { public Account getAccount() {
return this.account.getFullJid(); return this.account;
} }
public String getCounterPart() { public String getCounterPart() {
@ -253,13 +251,14 @@ public class JingleConnection implements Downloadable {
} }
public void init(Account account, JinglePacket packet) { public void init(Account account, JinglePacket packet) {
this.status = STATUS_INITIATED; this.mJingleStatus = JINGLE_STATUS_INITIATED;
Conversation conversation = this.mXmppConnectionService Conversation conversation = this.mXmppConnectionService
.findOrCreateConversation(account, .findOrCreateConversation(account,
packet.getFrom().split("/", 2)[0], false); packet.getFrom().split("/", 2)[0], false);
this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE);
this.message.setStatus(Message.STATUS_RECEIVED);
this.message.setType(Message.TYPE_IMAGE); this.message.setType(Message.TYPE_IMAGE);
this.message.setStatus(Message.STATUS_RECEIVED_OFFER); this.mStatus = Downloadable.STATUS_OFFER;
this.message.setDownloadable(this); this.message.setDownloadable(this);
String[] fromParts = packet.getFrom().split("/", 2); String[] fromParts = packet.getFrom().split("/", 2);
this.message.setPresence(fromParts[1]); this.message.setPresence(fromParts[1]);
@ -304,7 +303,8 @@ public class JingleConnection implements Downloadable {
if (supportedFile) { if (supportedFile) {
long size = Long.parseLong(fileSize.getContent()); long size = Long.parseLong(fileSize.getContent());
message.setBody(Long.toString(size)); message.setBody(Long.toString(size));
conversation.getMessages().add(message); conversation.add(message);
mXmppConnectionService.updateConversationUi();
if (size <= this.mJingleConnectionManager if (size <= this.mJingleConnectionManager
.getAutoAcceptFileSize()) { .getAutoAcceptFileSize()) {
Log.d(Config.LOGTAG, "auto accepting file from " Log.d(Config.LOGTAG, "auto accepting file from "
@ -323,7 +323,7 @@ public class JingleConnection implements Downloadable {
.push(message); .push(message);
} }
this.file = this.mXmppConnectionService.getFileBackend() this.file = this.mXmppConnectionService.getFileBackend()
.getJingleFile(message, false); .getFile(message, false);
if (message.getEncryption() == Message.ENCRYPTION_OTR) { if (message.getEncryption() == Message.ENCRYPTION_OTR) {
byte[] key = conversation.getSymmetricKey(); byte[] key = conversation.getSymmetricKey();
if (key == null) { if (key == null) {
@ -350,12 +350,13 @@ public class JingleConnection implements Downloadable {
} }
private void sendInitRequest() { private void sendInitRequest() {
this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED);
JinglePacket packet = this.bootstrapPacket("session-initiate"); JinglePacket packet = this.bootstrapPacket("session-initiate");
Content content = new Content(this.contentCreator, this.contentName); Content content = new Content(this.contentCreator, this.contentName);
if (message.getType() == Message.TYPE_IMAGE) { if (message.getType() == Message.TYPE_IMAGE) {
content.setTransportId(this.transportId); content.setTransportId(this.transportId);
this.file = this.mXmppConnectionService.getFileBackend() this.file = this.mXmppConnectionService.getFileBackend().getFile(
.getJingleFile(message, false); message, false);
if (message.getEncryption() == Message.ENCRYPTION_OTR) { if (message.getEncryption() == Message.ENCRYPTION_OTR) {
Conversation conversation = this.message.getConversation(); Conversation conversation = this.message.getConversation();
this.mXmppConnectionService.renewSymmetricKey(conversation); this.mXmppConnectionService.renewSymmetricKey(conversation);
@ -369,7 +370,7 @@ public class JingleConnection implements Downloadable {
content.socks5transport().setChildren(getCandidatesAsElements()); content.socks5transport().setChildren(getCandidatesAsElements());
packet.setContent(content); packet.setContent(content);
this.sendJinglePacket(packet); this.sendJinglePacket(packet);
this.status = STATUS_INITIATED; this.mJingleStatus = JINGLE_STATUS_INITIATED;
} }
} }
@ -382,8 +383,9 @@ public class JingleConnection implements Downloadable {
} }
private void sendAccept() { private void sendAccept() {
status = STATUS_ACCEPTED; mJingleStatus = JINGLE_STATUS_ACCEPTED;
mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVING); this.mStatus = Downloadable.STATUS_DOWNLOADING;
mXmppConnectionService.updateConversationUi();
this.mJingleConnectionManager.getPrimaryCandidate(this.account, this.mJingleConnectionManager.getPrimaryCandidate(this.account,
new OnPrimaryCandidateFound() { new OnPrimaryCandidateFound() {
@ -457,7 +459,7 @@ public class JingleConnection implements Downloadable {
Content content = packet.getJingleContent(); Content content = packet.getJingleContent();
mergeCandidates(JingleCandidate.parse(content.socks5transport() mergeCandidates(JingleCandidate.parse(content.socks5transport()
.getChildren())); .getChildren()));
this.status = STATUS_ACCEPTED; this.mJingleStatus = JINGLE_STATUS_ACCEPTED;
mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND);
this.connectNextCandidate(); this.connectNextCandidate();
return true; return true;
@ -492,7 +494,8 @@ public class JingleConnection implements Downloadable {
} else if (content.socks5transport().hasChild("candidate-error")) { } else if (content.socks5transport().hasChild("candidate-error")) {
Log.d(Config.LOGTAG, "received candidate error"); Log.d(Config.LOGTAG, "received candidate error");
this.receivedCandidate = true; this.receivedCandidate = true;
if ((status == STATUS_ACCEPTED) && (this.sentCandidate)) { if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
&& (this.sentCandidate)) {
this.connect(); this.connect();
} }
return true; return true;
@ -504,7 +507,8 @@ public class JingleConnection implements Downloadable {
JingleCandidate candidate = getCandidate(cid); JingleCandidate candidate = getCandidate(cid);
candidate.flagAsUsedByCounterpart(); candidate.flagAsUsedByCounterpart();
this.receivedCandidate = true; this.receivedCandidate = true;
if ((status == STATUS_ACCEPTED) && (this.sentCandidate)) { if ((mJingleStatus == JINGLE_STATUS_ACCEPTED)
&& (this.sentCandidate)) {
this.connect(); this.connect();
} else { } else {
Log.d(Config.LOGTAG, Log.d(Config.LOGTAG,
@ -532,7 +536,7 @@ public class JingleConnection implements Downloadable {
this.sendFallbackToIbb(); this.sendFallbackToIbb();
} }
} else { } else {
this.status = STATUS_TRANSMITTING; this.mJingleStatus = JINGLE_STATUS_TRANSMITTING;
if (connection.needsActivation()) { if (connection.needsActivation()) {
if (connection.getCandidate().isOurs()) { if (connection.getCandidate().isOurs()) {
Log.d(Config.LOGTAG, "candidate " Log.d(Config.LOGTAG, "candidate "
@ -619,13 +623,15 @@ public class JingleConnection implements Downloadable {
packet.setReason(reason); packet.setReason(reason);
this.sendJinglePacket(packet); this.sendJinglePacket(packet);
this.disconnect(); this.disconnect();
this.status = STATUS_FINISHED; this.mJingleStatus = JINGLE_STATUS_FINISHED;
this.mXmppConnectionService.markMessage(this.message, this.message.setStatus(Message.STATUS_RECEIVED);
Message.STATUS_RECEIVED); this.message.setDownloadable(null);
this.mXmppConnectionService.updateMessage(message);
this.mJingleConnectionManager.finishConnection(this); this.mJingleConnectionManager.finishConnection(this);
} }
private void sendFallbackToIbb() { private void sendFallbackToIbb() {
Log.d(Config.LOGTAG, "sending fallback to ibb");
JinglePacket packet = this.bootstrapPacket("transport-replace"); JinglePacket packet = this.bootstrapPacket("transport-replace");
Content content = new Content(this.contentCreator, this.contentName); Content content = new Content(this.contentCreator, this.contentName);
this.transportId = this.mJingleConnectionManager.nextRandomId(); this.transportId = this.mJingleConnectionManager.nextRandomId();
@ -637,6 +643,7 @@ public class JingleConnection implements Downloadable {
} }
private boolean receiveFallbackToIbb(JinglePacket packet) { private boolean receiveFallbackToIbb(JinglePacket packet) {
Log.d(Config.LOGTAG, "receiving fallack to ibb");
String receivedBlockSize = packet.getJingleContent().ibbTransport() String receivedBlockSize = packet.getJingleContent().ibbTransport()
.getAttribute("block-size"); .getAttribute("block-size");
if (receivedBlockSize != null) { if (receivedBlockSize != null) {
@ -691,7 +698,7 @@ public class JingleConnection implements Downloadable {
} }
private void receiveSuccess() { private void receiveSuccess() {
this.status = STATUS_FINISHED; this.mJingleStatus = JINGLE_STATUS_FINISHED;
this.mXmppConnectionService.markMessage(this.message, this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND); Message.STATUS_SEND);
this.disconnect(); this.disconnect();
@ -699,20 +706,15 @@ public class JingleConnection implements Downloadable {
} }
public void cancel() { public void cancel() {
this.status = STATUS_CANCELED; this.mJingleStatus = JINGLE_STATUS_CANCELED;
this.disconnect(); this.disconnect();
if (this.message != null) { if (this.message != null) {
if (this.responder.equals(account.getFullJid())) { if (this.responder.equals(account.getFullJid())) {
this.mXmppConnectionService.markMessage(this.message, this.mStatus = Downloadable.STATUS_FAILED;
Message.STATUS_RECEPTION_FAILED); this.mXmppConnectionService.updateConversationUi();
} else { } else {
if (this.status == STATUS_INITIATED) { this.mXmppConnectionService.markMessage(this.message,
this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED);
Message.STATUS_SEND_REJECTED);
} else {
this.mXmppConnectionService.markMessage(this.message,
Message.STATUS_SEND_FAILED);
}
} }
} }
this.mJingleConnectionManager.finishConnection(this); this.mJingleConnectionManager.finishConnection(this);
@ -789,7 +791,7 @@ public class JingleConnection implements Downloadable {
.setAttribute("cid", cid); .setAttribute("cid", cid);
packet.setContent(content); packet.setContent(content);
this.sentCandidate = true; this.sentCandidate = true;
if ((receivedCandidate) && (status == STATUS_ACCEPTED)) { if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
connect(); connect();
} }
this.sendJinglePacket(packet); this.sendJinglePacket(packet);
@ -802,7 +804,7 @@ public class JingleConnection implements Downloadable {
content.socks5transport().addChild("candidate-error"); content.socks5transport().addChild("candidate-error");
packet.setContent(content); packet.setContent(content);
this.sentCandidate = true; this.sentCandidate = true;
if ((receivedCandidate) && (status == STATUS_ACCEPTED)) { if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) {
connect(); connect();
} }
this.sendJinglePacket(packet); this.sendJinglePacket(packet);
@ -816,8 +818,8 @@ public class JingleConnection implements Downloadable {
return this.responder; return this.responder;
} }
public int getStatus() { public int getJingleStatus() {
return this.status; return this.mJingleStatus;
} }
private boolean equalCandidateExists(JingleCandidate candidate) { private boolean equalCandidateExists(JingleCandidate candidate) {
@ -867,17 +869,34 @@ public class JingleConnection implements Downloadable {
return this.transport; return this.transport;
} }
public void start() { public boolean start() {
if (status == STATUS_INITIATED) { if (account.getStatus() == Account.STATUS_ONLINE) {
new Thread(new Runnable() { if (mJingleStatus == JINGLE_STATUS_INITIATED) {
new Thread(new Runnable() {
@Override @Override
public void run() { public void run() {
sendAccept(); sendAccept();
} }
}).start(); }).start();
}
return true;
} else { } 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.Config;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.services.AbstractConnectionManager;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnIqPacketReceived;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket;
public class JingleConnectionManager { public class JingleConnectionManager extends AbstractConnectionManager {
private XmppConnectionService xmppConnectionService;
private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>(); private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>();
private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>(); private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>();
@ -28,7 +26,7 @@ public class JingleConnectionManager {
private SecureRandom random = new SecureRandom(); private SecureRandom random = new SecureRandom();
public JingleConnectionManager(XmppConnectionService service) { public JingleConnectionManager(XmppConnectionService service) {
this.xmppConnectionService = service; super(service);
} }
public void deliverPacket(Account account, JinglePacket packet) { public void deliverPacket(Account account, JinglePacket packet) {
@ -38,7 +36,7 @@ public class JingleConnectionManager {
connections.add(connection); connections.add(connection);
} else { } else {
for (JingleConnection connection : connections) { for (JingleConnection connection : connections) {
if (connection.getAccountJid().equals(account.getFullJid()) if (connection.getAccount() == account
&& connection.getSessionId().equals( && connection.getSessionId().equals(
packet.getSessionId()) packet.getSessionId())
&& connection.getCounterPart().equals(packet.getFrom())) { && connection.getCounterPart().equals(packet.getFrom())) {
@ -46,8 +44,13 @@ public class JingleConnectionManager {
return; return;
} }
} }
account.getXmppConnection().sendIqPacket( IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR);
packet.generateRespone(IqPacket.TYPE_ERROR), null); 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); this.connections.remove(connection);
} }
public XmppConnectionService getXmppConnectionService() {
return this.xmppConnectionService;
}
public void getPrimaryCandidate(Account account, public void getPrimaryCandidate(Account account,
final OnPrimaryCandidateFound listener) { final OnPrimaryCandidateFound listener) {
if (!this.primaryCandidates.containsKey(account.getJid())) { if (!this.primaryCandidates.containsKey(account.getJid())) {
@ -128,16 +127,6 @@ public class JingleConnectionManager {
return new BigInteger(50, random).toString(32); 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) { public void deliverIbbPacket(Account account, IqPacket packet) {
String sid = null; String sid = null;
Element payload = null; Element payload = null;
@ -152,7 +141,8 @@ public class JingleConnectionManager {
} }
if (sid != null) { if (sid != null) {
for (JingleConnection connection : connections) { for (JingleConnection connection : connections) {
if (connection.hasTransportId(sid)) { if (connection.getAccount() == account
&& connection.hasTransportId(sid)) {
JingleTransport transport = connection.getTransport(); JingleTransport transport = connection.getTransport();
if (transport instanceof JingleInbandTransport) { if (transport instanceof JingleInbandTransport) {
JingleInbandTransport inbandTransport = (JingleInbandTransport) transport; JingleInbandTransport inbandTransport = (JingleInbandTransport) transport;
@ -170,7 +160,7 @@ public class JingleConnectionManager {
public void cancelInTransmission() { public void cancelInTransmission() {
for (JingleConnection connection : this.connections) { for (JingleConnection connection : this.connections) {
if (connection.getStatus() == JingleConnection.STATUS_TRANSMITTING) { if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) {
connection.cancel(); 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; package eu.siacs.conversations.xmpp.jingle;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
@ -10,6 +9,7 @@ import java.util.Arrays;
import android.util.Base64; import android.util.Base64;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.DownloadableFile;
import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.CryptoHelper;
import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnIqPacketReceived;
@ -26,7 +26,7 @@ public class JingleInbandTransport extends JingleTransport {
private boolean established = false; private boolean established = false;
private JingleFile file; private DownloadableFile file;
private InputStream fileInputStream = null; private InputStream fileInputStream = null;
private OutputStream fileOutputStream; private OutputStream fileOutputStream;
@ -77,7 +77,7 @@ public class JingleInbandTransport extends JingleTransport {
} }
@Override @Override
public void receive(JingleFile file, public void receive(DownloadableFile file,
OnFileTransmissionStatusChanged callback) { OnFileTransmissionStatusChanged callback) {
this.onFileTransmissionStatusChanged = callback; this.onFileTransmissionStatusChanged = callback;
this.file = file; this.file = file;
@ -86,7 +86,7 @@ public class JingleInbandTransport extends JingleTransport {
digest.reset(); digest.reset();
file.getParentFile().mkdirs(); file.getParentFile().mkdirs();
file.createNewFile(); file.createNewFile();
this.fileOutputStream = getOutputStream(file); this.fileOutputStream = file.createOutputStream();
if (this.fileOutputStream == null) { if (this.fileOutputStream == null) {
callback.onFileTransferAborted(); callback.onFileTransferAborted();
return; return;
@ -100,20 +100,19 @@ public class JingleInbandTransport extends JingleTransport {
} }
@Override @Override
public void send(JingleFile file, OnFileTransmissionStatusChanged callback) { public void send(DownloadableFile file,
OnFileTransmissionStatusChanged callback) {
this.onFileTransmissionStatusChanged = callback; this.onFileTransmissionStatusChanged = callback;
this.file = file; this.file = file;
try { try {
this.digest = MessageDigest.getInstance("SHA-1"); this.digest = MessageDigest.getInstance("SHA-1");
this.digest.reset(); this.digest.reset();
fileInputStream = this.getInputStream(file); fileInputStream = this.file.createInputStream();
if (fileInputStream == null) { if (fileInputStream == null) {
callback.onFileTransferAborted(); callback.onFileTransferAborted();
return; return;
} }
this.sendNextBlock(); this.sendNextBlock();
} catch (FileNotFoundException e) {
callback.onFileTransferAborted();
} catch (NoSuchAlgorithmException e) { } catch (NoSuchAlgorithmException e) {
callback.onFileTransferAborted(); callback.onFileTransferAborted();
} }

View file

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

View file

@ -1,88 +1,13 @@
package eu.siacs.conversations.xmpp.jingle; package eu.siacs.conversations.xmpp.jingle;
import java.io.FileInputStream; import eu.siacs.conversations.entities.DownloadableFile;
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;
public abstract class JingleTransport { public abstract class JingleTransport {
public abstract void connect(final OnTransportConnected callback); public abstract void connect(final OnTransportConnected callback);
public abstract void receive(final JingleFile file, public abstract void receive(final DownloadableFile file,
final OnFileTransmissionStatusChanged callback); final OnFileTransmissionStatusChanged callback);
public abstract void send(final JingleFile file, public abstract void send(final DownloadableFile file,
final OnFileTransmissionStatusChanged callback); 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; package eu.siacs.conversations.xmpp.jingle;
import eu.siacs.conversations.entities.DownloadableFile;
public interface OnFileTransmissionStatusChanged { public interface OnFileTransmissionStatusChanged {
public void onFileTransmitted(JingleFile file); public void onFileTransmitted(DownloadableFile file);
public void onFileTransferAborted(); public void onFileTransferAborted();
} }

View file

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