diff options
Diffstat (limited to '')
82 files changed, 3092 insertions, 1950 deletions
diff --git a/.gitmodules b/.gitmodules index d0f56166..7ddaefe6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,4 +7,4 @@ url = https://github.com/open-keychain/openpgp-api-lib.git [submodule "libs/MemorizingTrustManager"] path = libs/MemorizingTrustManager - url = https://github.com/ge0rg/MemorizingTrustManager + url = https://github.com/iNPUTmice/MemorizingTrustManager.git diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ce4ea5df..a66de8ba 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,9 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" package="eu.siacs.conversations" - android:versionCode="31" - android:versionName="0.7.3" > + android:versionCode="32" + android:versionName="0.8-alpha" > <uses-sdk android:minSdkVersion="14" @@ -22,14 +23,10 @@ android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" + tools:replace="android:label" android:theme="@style/ConversationsTheme" > <service android:name="eu.siacs.conversations.services.XmppConnectionService" /> - <provider - android:name="eu.siacs.conversations.services.ImageProvider" - android:authorities="eu.siacs.conversations.images" - android:exported="true" /> - <receiver android:name="eu.siacs.conversations.services.EventReceiver" > <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> @@ -1,19 +1,22 @@ -#Conversations -Conversations - the very last word in instant messaging +# Conversations + +Conversations: the very last word in instant messaging [![Google Play](http://developer.android.com/images/brand/en_generic_rgb_wo_45.png)](https://play.google.com/store/apps/details?id=eu.siacs.conversations) ![screenshots](https://raw.githubusercontent.com/siacs/Conversations/master/screenshots.png) -##Design principles +## Design principles + * Be as beautiful and easy to use as possible without sacrificing security or privacy * Rely on existing, well established protocols (XMPP) * Do not require a Google Account or specifically Google Cloud Messaging (GCM) -* Require as little permissons as possible +* Require as few permissions as possible + +## Features -##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 * Indication when your contact has read your message * Intuitive UI that follows Android Design guidelines @@ -21,47 +24,56 @@ Conversations - the very last word in instant messaging * Syncs with desktop client * Conferences (with support for bookmarks) * Address book integration -* Multiple Accounts / unified inbox +* Multiple accounts / unified inbox * Very low impact on battery life -###XMPP Features -Conversations works with every XMPP server out there. However XMPP is an extensible -protocol. These extensions are standardized as well in so called XEP’s. -Conversations supports a couple of those to make the overall user experience better. There is a -chance that your current XMPP server does not support these extensions. -Therefore to get the most out of Conversations you should consider either switching to an -XMPP server that does or - even better - run your own XMPP server for you and -your friends. -These XEPs are - as of now: -* XEP-0065: SOCKS5 Bytestreams - or rather mod_proxy65. Will be used to transfer files if both parties are behind a firewall (NAT). +### XMPP Features + +Conversations works with every XMPP server out there. However XMPP is an +extensible protocol. These extensions are standardized as well in so called +XEP's. Conversations supports a couple of these to make the overall user +experience better. There is a chance that your current XMPP server does not +support these extensions; therefore to get the most out of Conversations you +should consider either switching to an XMPP server that does or — even better — +run your own XMPP server for you and your friends. These XEP's are: + +* XEP-0065: SOCKS5 Bytestreams (or mod_proxy65). Will be used to transfer + files if both parties are behind a firewall (NAT). * XEP-0138: Stream Compression saves bandwidth * XEP-0163: Personal Eventing Protocol for avatars -* XEP-0198: Stream Management allows XMPP to survive small network outages and changes of the underlying TCP connection. +* XEP-0198: Stream Management allows XMPP to survive small network outages and + changes of the underlying TCP connection. * XEP-0280: Message Carbons which automatically syncs the messages you send to your desktop client and thus allows you to switch seamlessly from your mobile client to your desktop client and back within one conversation. * XEP-0237: Roster Versioning mainly to save bandwidth on poor mobile connections * XEP-0352: Client State Indication let the server know whether or not Conversations is in the background. Allows the server to save bandwidth by - withholding unimportent packages. + withholding unimportant packages. + +## Team + +#### Head of Development -##Team -####Head of Development * [Daniel Gultsch](https://github.com/inputmice) -####Code Contributions +#### Code Contributions + (In order of appearance) + * [Rene Treffer](https://github.com/rtreffer) * [Andreas Straub](https://github.com/strb) * [Alethea Butler](https://github.com/alethea) * [M. Dietrich](https://github.com/emdete) * [betheg](https://github.com/betheg) -####Logo +#### Logo + * [Diego Turtulici](http://efesto.eigenlab.org/~diesys) -####Translations +#### Translations + * [Sergio Cárdenas](https://github.com/kruks23) (Spanish) * [Benoit Bouvarel](https://github.com/BenoitBouvarel) (French) * [Daniel Gultsch](https://github.com/iNPUTmice) (German) @@ -71,180 +83,215 @@ These XEPs are - as of now: * [Anders Sandblad](https://github.com/andersruneson) (Swedish) * [Aizaz AZ](http://www.linkedin.com/in/aizazhaider) (Chinese) -##FAQ -###General -####How do I install Conversations? +## FAQ + +### General + +#### How do I install Conversations? + Conversations is entirely open source and licensed under GPLv3. So if you are a -software developer you can check out the sources from github and use ant to +software developer you can check out the sources from GitHub and use ant to build your apk file. -The more convenient way - which not only gives you automatic updates but also -supports the further development of Conversations - is to buy the App in the Google -[Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations). -####I don't have a Google Account but I would still like to make a contribution -I accept donations over PayPal, BitCoin and Flattr. For donations via PayPal you can use the email address donate@siacs.eu or the button below. +The more convenient way — which not only gives you automatic updates but also +supports the further development of Conversations - is to buy the App in the +Google [Play Store](https://play.google.com/store/apps/details?id=eu.siacs.conversations). + +#### I don't have a Google Account but I would still like to make a contribution + +I accept donations over PayPal, Bitcoin and Flattr. For donations via PayPal you +can use the email address `donate@siacs.eu` or the button below. [![Donate with PayPal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CW3SYT3KG5PDL) -**Disclaimer:** I'm not a huge fan of PayPal and their business policies. For larger -contributions please get in touch with me beforehand and we can talk about bank -transfer (SEPA). +**Disclaimer:** I'm not a huge fan of PayPal and their business policies. For +larger contributions please get in touch with me beforehand and we can talk +about bank transfer (SEPA). + +My Bitcoin Address is: `1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu` + -My Bitcoin Address is: 1NxSU1YxYzJVDpX1rcESAA3NJki7kRgeeu +[![Flattr this!](http://api.flattr.com/button/flattr-badge-large.png)](https://flattr.com/submit/auto?user_id=inputmice&url=http%3A%2F%2Fconversations.siacs.eu&title=Conversations&tags=github&category=software) +#### How do I create an account? -[![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) +XMPP, like email, is a federated protocol which means that there is not one +company you can create an 'official XMPP account' with. Instead there are +hundreds, or even thousands, of provider out there. To find one use a web search +engine of your choice. Or maybe your university has one. Or you can run your +own. Or ask a friend to run one. Once you've found one, you can use +Conversations to create an account. Just select 'register new account on server' +within the create account dialog. -####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? -####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 +You can join our conference room on `conversations@conference.siacs.eu` A lot of people in there are able to answer basic questions about the usage of Conversations or can provide you with tips on running your own XMPP server. If you found a bug or your app crashes please read the Developer / Report Bugs section of this document. -####I need professional support with Conversations or setting up my server -I'm available for hire. Contact me at inputmice@siacs.eu +#### I need professional support with Conversations or setting up my server -####How does the address book integration work? -The address bock integration was designed to protect your privacy. Conversations +I'm available for hire. Contact me at `inputmice@siacs.eu`. + +#### How does the address book integration work? + +The address book integration was designed to protect your privacy. Conversations neither uploads contacts from your address book to your server nor fills your address book with unnecessary contacts from your online roster. If you manually add a Jabber ID to your phones address book Conversations will use the name and the profile picture of this contact. To make the process of adding Jabber IDs to your address book easier you can click on the profile picture in the contact -details within Conversations. This will start an add to address book intent with the jabber ID -as payload. This doesn’t require Conversations to have write permissions on your -address book but also doesn’t require you to copy past Jabber ID from one app to -another. - -####I get 'delivery failed' on my messages -If you get delivery failed on images it’s probably because the recipient lost -network connectivity during recepiton. In that case you can try it again at a +details within Conversations. This will start an "add to address book" intent +with the JID as the payload. This doesn't require Conversations to have write +permissions on your address book but also doesn't require you to copy/paste a +JID from one app to another. + +#### I get 'delivery failed' on my messages + +If you get delivery failed on images it's probably because the recipient lost +network connectivity during reception. In that case you can try it again at a later time. For text messages the answer to your question is a little bit more complex. -'delivery failed' on text messages is always something that is being reported by -the server. The most common reason for this is that the recipient failed to -resume a connection. When a client loses connectivity for a short time the client -usually has a five minute window to pick up that connection again. When the -client fails to do so because the network connectivity is out for longer than -that all messages sent to that client will be returned to the sender resulting -in a delivery failed. - -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 -that the recipient is offline and the server doesn’t provide offline storage. +When you see 'delivery failed' on text messages, it is always something that is +being reported by the server. The most common reason for this is that the +recipient failed to resume a connection. When a client loses connectivity for a +short time the client usually has a five minute window to pick up that +connection again. When the client fails to do so because the network +connectivity is out for longer than that all messages sent to that client will +be returned to the sender resulting in a delivery failed. + +Other less common reasons are that the message you sent didn't meet some +criteria enforced by the server (too large, too many). Another reason could be +that the recipient is offline and the server doesn't provide offline storage. Usually you are able to distinguish between these two groups in the fact that the first one happens always after some time and the second one happens almost instantly. -####Where can I see the status of my contacts? How can I set a status or priority -Status are a horrible metric. Setting them manually to a proper value rarely +#### Where can I see the status of my contacts? How can I set a status or priority? + +Statuses are a horrible metric. Setting them manually to a proper value rarely works because users are either lazy or just forget about them. Setting them automatically does not provide quality results either. Keyboard or mouse activity as indicator for example fails when the user is just looking at something (reading an article, watching a movie). Furthermore automatic setting -of status always implies an impact on your privacy. (Are you sure you want +of status always implies an impact on your privacy (are you sure you want everybody in your contact list to know that you have been using your computer at -4am?!) +4am‽). In the past status has been used to judge the likelihood of whether or not your messages are being read. This is no longer necessary. With Chat Markers (XEP-0333, supported by Conversations since 0.4) we have the ability to **know** -whether or not your messages are being read. -Similar things can be said for priorities. In the past priorities have been used -(By servers, not by clients!) to route your messages to one specific client. -With carbon messages (XEP-0280, supported by Conversations since 0.1) this is no -longer necessary. Using priorities to route OTR messages isn't pratical either -because they are not changeable on the fly. Metrics like last active client -(the client which sent the last message) are much better. +whether or not your messages are being read. Similar things can be said for +priorities. In the past priorities have been used (by servers, not by clients!) +to route your messages to one specific client. With carbon messages (XEP-0280, +supported by Conversations since 0.1) this is no longer necessary. Using +priorities to route OTR messages isn't practical either because they are not +changeable on the fly. Metrics like last active client (the client which sent +the last message) are much better. Unfortunately these modern replacements for legacy XMPP features are not widely adopted. However Conversations should be an instant messenger for the future and instead of making Conversations compatible with the past we should work on -implementing new, improved technologies into other XMPP clients as well. +implementing new, improved technologies and getting them into other XMPP clients +as well. Making these status and priority optional isn't a solution either because Conversations is trying to get rid of old behaviours and set an example for other clients. -####Conversations is missing a certain feature -I'm open for new feature suggestions. You can use the issue tracker on github. -Please take some time to browse through the issues to see if someone else -already suggested it. Be assured that I read each and every ticket. If I like it -I will leave it open until it's implemented. If I don't like it I will close -it. (Usually with a short comment). If I don't comment on an feature request -that's probably a good sign because this means I agree with you. Commenting with -+1 on either open or closed issues won't change my mind nor will it accelerate the -development. - -####You closed my feature request but I want it really really badly +#### Conversations is missing a certain feature + +I'm open for new feature suggestions. You can use the [issue tracker][issues] on +GitHub. Please take some time to browse through the issues to see if someone +else already suggested it. Be assured that I read each and every ticket. If I +like it I will leave it open until it's implemented. If I don't like it I will +close it (usually with a short comment). If I don't comment on an feature +request that's probably a good sign because this means I agree with you. +Commenting with +1 on either open or closed issues won't change my mind, nor +will it accelerate the development. + +#### You closed my feature request but I want it really really badly + Just write it yourself and send me a pull request. If I like it I will happily merge it if I don't at least you and like minded people get to enjoy it. -####I need a feature and I need it now! -I am available for hire. Contact me JID: inputmice@siacs.eu - -###Security -####Why are there two end-to-end encryption methods and which one should I choose? -In most cases OTR should be the encryption method of choice. It works out of the box with most contacts as long as they are online. -However PGP can be in some cases (carbonated messages to multiple clients) be -more flexible. -####How do I use openPGP -Before you continue reading you should notice that the openPGP support in -Conversations is marked as experimental. This is not because it will make the app -unstable but because the fundamental concepts of PGP aren't ready for a -widespread use. The way PGP works is that you trust Key IDs instead of XMPP- or email addresses. So in theory your contact list should consist of Public-Key-IDs instead of email addresses. But of course no email or xmpp client out there implements these concepts. Plus PGP in the context of instant messaging has a couple of downsides. It is vulnerable to replay attacks, it is rather verbose, and decrypting and encrypting takes longer than OTR. It is however asynchronous and works well with carbonated messages. - -To use openpgp you have to install the opensource app OpenKeychain (www.openkeychain.org) and then long press on the account in manage accounts and choose renew PGP announcement from the contextual menu. -####How does the encryption for conferences work? -For conferences the only supported encryption method is OpenPGP. (OTR does not -work with multiple participants.) Every participant has to announce their -OpenPGP key. (See answer above). If you would like to send encrypted messages to +#### I need a feature and I need it now! + +I am available for hire. Contact me via XMPP: `inputmice@siacs.eu` + +### Security + +#### Why are there two end-to-end encryption methods and which one should I choose? + +In most cases OTR should be the encryption method of choice. It works out of the +box with most contacts as long as they are online. However PGP can, in some +cases, (message carbons to multiple clients) be more flexible. + +#### How do I use OpenPGP + +Before you continue reading you should note that the OpenPGP support in +Conversations is experimental. This is not because it will make the app unstable +but because the fundamental concepts of PGP aren't ready for widespread use. +The way PGP works is that you trust Key IDs instead of JID's or email addresses. +So in theory your contact list should consist of Public-Key-IDs instead of +JID's. But of course no email or XMPP client out there implements these +concepts. Plus PGP in the context of instant messaging has a couple of +downsides: It is vulnerable to replay attacks, it is rather verbose, and +decrypting and encrypting takes longer than OTR. It is however asynchronous and +works well with message carbons. + +To use OpenPGP you have to install the open source app +[OpenKeychain](www.openkeychain.org) and then long press on the account in +manage accounts and choose renew PGP announcement from the contextual menu. + +#### How does the encryption for conferences work? + +For conferences the only supported encryption method is OpenPGP (OTR does not +work with multiple participants). Every participant has to announce their +OpenPGP key (see answer above). If you would like to send encrypted messages to a conference you have to make sure that you have every participant's public key in your OpenKeychain. Right now there is no check in Conversations to ensure that. You have to take care of that yourself. Go to the conference details and touch every key id (The hexadecimal number below a contact). This will send you -to OpenKeychain which will assist you on adding the key. -This works best in very small conferences with contacts you are already using -OpenPGP with. This feature is regarded experimental. Conversations is the only -client that uses XEP-0027 with conferences. (The XEP neither specifically allows -nor disallows this.) -###Development -####How do I build Conversations +to OpenKeychain which will assist you on adding the key. This works best in +very small conferences with contacts you are already using OpenPGP with. This +feature is regarded experimental. Conversations is the only client that uses +XEP-0027 with conferences. (The XEP neither specifically allows nor disallows +this.) + +### Development + +#### How do I build Conversations + Make sure to have ANDROID_HOME point to your Android SDK -``` -git clone https://github.com/siacs/Conversations.git -cd Conversations -git submodule update --init --recursive -ant clean -ant debug -``` -####How do I debug Conversations + + git clone https://github.com/siacs/Conversations.git + cd Conversations + git submodule update --init --recursive + ant clean + ant debug + +#### How do I debug Conversations + If something goes wrong Conversations usually exposes very little information in -the UI. (Other than the fact that something didn't work) -However with adb (android debug bridge) you squeeze some more information out of -Conversations. These information are especially useful if you are experiencing -troubles with your connection or with file transfer. -```` -adb -d logcat -v time -s conversations -```` -####I found a bug -Please report it to our issue tracker. If your app crashes please provide a -stack trace. If you are experiencing missbehaviour please provide detailed -steps to reproduce. -Always mention whether you are running the latest Play Store version or the -current HEAD. -If you are having problems connecting to your XMPP server your file transfer -doesn’t work as expected please always include a logcat debug output with your -issue. (See above) +the UI (other than the fact that something didn't work). However with adb +(android debug bridge) you squeeze some more information out of Conversations. +These information are especially useful if you are experiencing trouble with +your connection or with file transfer. + + adb -d logcat -v time -s conversations + +#### I found a bug + +Please report it to our [issue tracker][issues]. If your app crashes please +provide a stack trace. If you are experiencing misbehaviour please provide +detailed steps to reproduce. Always mention whether you are running the latest +Play Store version or the current HEAD. If you are having problems connecting to +your XMPP server your file transfer doesn’t work as expected please always +include a logcat debug output with your issue (see above). + +[issues]: https://github.com/siacs/Conversations/issues diff --git a/art/ic_action_copy.svg b/art/ic_action_copy.svg deleted file mode 100644 index 485fd2ed..00000000 --- a/art/ic_action_copy.svg +++ /dev/null @@ -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> diff --git a/art/logo.png b/art/logo.png Binary files differnew file mode 100644 index 00000000..a8ab6176 --- /dev/null +++ b/art/logo.png diff --git a/art/render.rb b/art/render.rb index a78a2f47..2847891d 100755 --- a/art/render.rb +++ b/art/render.rb @@ -1,6 +1,16 @@ #!/bin/env ruby -resolutions={'mdpi'=> 1, 'hdpi' => 1.5, 'xhdpi' => 2, 'xxhdpi' => 3} -images = { 'conversations.svg' => ['ic_launcher', 48], 'conversations_baloon.svg' => ['ic_activity', 32], 'conversations_mono.svg' => ['ic_notification', 24], 'ic_received_indicator.svg' => ['ic_received_indicator', 12], 'ic_action_copy.svg' => ['ic_action_copy', 32] } +resolutions={ + 'mdpi'=> 1, + 'hdpi' => 1.5, + 'xhdpi' => 2, + 'xxhdpi' => 3, + } +images = { + 'conversations.svg' => ['ic_launcher', 48], + 'conversations_baloon.svg' => ['ic_activity', 32], + 'conversations_mono.svg' => ['ic_notification', 24], + 'ic_received_indicator.svg' => ['ic_received_indicator', 12], + } images.each do |source, result| resolutions.each do |name, factor| size = factor * result[1] diff --git a/libs/MemorizingTrustManager b/libs/MemorizingTrustManager -Subproject 452f70208f0dd5f9e56376944e96f5c10704245 +Subproject fad835037adc1bd313bb56b694426fca4eb6734 diff --git a/res/drawable-hdpi/ic_action_copy.png b/res/drawable-hdpi/ic_action_copy.png Binary files differindex b47bb69c..22327391 100644 --- a/res/drawable-hdpi/ic_action_copy.png +++ b/res/drawable-hdpi/ic_action_copy.png diff --git a/res/drawable-mdpi/ic_action_copy.png b/res/drawable-mdpi/ic_action_copy.png Binary files differindex 75788f1f..71348202 100644 --- a/res/drawable-mdpi/ic_action_copy.png +++ b/res/drawable-mdpi/ic_action_copy.png diff --git a/res/drawable-xhdpi/ic_action_copy.png b/res/drawable-xhdpi/ic_action_copy.png Binary files differindex a45423f6..5ddf1513 100644 --- a/res/drawable-xhdpi/ic_action_copy.png +++ b/res/drawable-xhdpi/ic_action_copy.png diff --git a/res/drawable-xxhdpi/ic_action_copy.png b/res/drawable-xxhdpi/ic_action_copy.png Binary files differindex 3a0e8449..a0508df8 100644 --- a/res/drawable-xxhdpi/ic_action_copy.png +++ b/res/drawable-xxhdpi/ic_action_copy.png diff --git a/res/drawable/infocard_border.xml b/res/drawable/infocard_border.xml new file mode 100644 index 00000000..7c7ded57 --- /dev/null +++ b/res/drawable/infocard_border.xml @@ -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>
\ No newline at end of file diff --git a/res/drawable/section_header.xml b/res/drawable/section_header.xml deleted file mode 100644 index 9db04f90..00000000 --- a/res/drawable/section_header.xml +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/res/layout/activity_contact_details.xml b/res/layout/activity_contact_details.xml index f4fd9c1e..f7cb2198 100644 --- a/res/layout/activity_contact_details.xml +++ b/res/layout/activity_contact_details.xml @@ -1,126 +1,112 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/primarybackground" > + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@color/secondarybackground" > <LinearLayout - android:layout_width="match_parent" + android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > - <TextView - style="@style/sectionHeader" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="8dp" - android:text="@string/action_contact_details" - android:textColor="@color/primarytext" /> - <RelativeLayout - android:layout_width="wrap_content" + android:layout_width="fill_parent" android:layout_height="wrap_content" - android:minHeight="88dp" - android:padding="8dp" > + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:padding="16dp" > <QuickContactBadge android:id="@+id/details_contact_badge" android:layout_width="72dp" android:layout_height="72dp" - android:layout_centerVertical="true" + android:layout_alignParentTop="true" android:scaleType="centerCrop" /> <LinearLayout android:id="@+id/details_jidbox" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerVertical="true" + android:layout_marginLeft="16dp" android:layout_toRightOf="@+id/details_contact_badge" - android:orientation="vertical" - android:paddingLeft="8dp" > + android:orientation="vertical" > <TextView android:id="@+id/details_contactjid" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingLeft="8dp" - android:singleLine="true" + android:text="@string/account_settings_example_jabber_id" android:textColor="@color/primarytext" - android:textSize="?attr/TextSizeBody" /> + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold" /> - <TextView - android:id="@+id/details_contactstatus" + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" > + + <TextView + android:id="@+id/details_contactstatus" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text=" · " + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + + <TextView + android:id="@+id/details_lastseen" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> + </LinearLayout> + + <CheckBox + android:id="@+id/details_send_presence" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingLeft="16dp" + android:layout_marginTop="8dp" + android:text="@string/send_presence_updates" android:textColor="@color/primarytext" - android:textSize="?attr/TextSizeHeadline" - android:textStyle="bold" /> + android:textSize="?attr/TextSizeBody" /> - <TextView - android:id="@+id/details_lastseen" + <CheckBox + android:id="@+id/details_receive_presence" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingLeft="8dp" - android:singleLine="true" + android:text="@string/receive_presence_updates" android:textColor="@color/primarytext" android:textSize="?attr/TextSizeBody" /> </LinearLayout> - </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" /> + <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> <LinearLayout android:id="@+id/details_contact_keys" - android:layout_width="match_parent" + android:layout_width="fill_parent" android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" android:divider="?android:dividerHorizontal" android:orientation="vertical" + android:padding="8dp" android:showDividers="middle" > </LinearLayout> </LinearLayout> diff --git a/res/layout/activity_edit_account.xml b/res/layout/activity_edit_account.xml index 91bda7b1..97289628 100644 --- a/res/layout/activity_edit_account.xml +++ b/res/layout/activity_edit_account.xml @@ -2,7 +2,7 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="@color/primarybackground" > + android:background="@color/secondarybackground" > <ScrollView android:layout_width="fill_parent" @@ -19,8 +19,10 @@ android:id="@+id/editor" android:layout_width="fill_parent" android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" android:orientation="vertical" - android:padding="8dp" > + android:padding="16dp" > <TextView android:layout_width="wrap_content" @@ -34,7 +36,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/account_settings_example_jabber_id" - android:inputType="textEmailAddress" /> + android:inputType="textEmailAddress" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> <TextView android:layout_width="wrap_content" @@ -49,7 +54,10 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="@string/password" - android:inputType="textPassword" /> + android:inputType="textPassword" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> <CheckBox android:id="@+id/account_register_new" @@ -76,31 +84,25 @@ android:layout_marginTop="8dp" android:hint="@string/confirm_password" android:inputType="textPassword" - android:visibility="gone" /> + android:visibility="gone" + android:textColor="@color/primarytext" + android:textColorHint="@color/secondarytext" + android:textSize="?attr/TextSizeBody" /> </LinearLayout> - <LinearLayout + <LinearLayout android:id="@+id/stats" android:layout_width="fill_parent" android:layout_height="fill_parent" - android:layout_marginTop="8dp" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" android:orientation="vertical" android:padding="16dp" android:visibility="gone" > - <TextView - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:gravity="center_horizontal" - android:text="@string/additional_information" - android:textColor="@color/secondarytext" - android:textSize="?attr/TextSizeHeadline" - android:textStyle="bold" /> - <TableLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="8dp" android:stretchColumns="1" > <TableRow @@ -110,13 +112,17 @@ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/server_info_session_established" /> + android:text="@string/server_info_session_established" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> <TextView android:id="@+id/session_est" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" /> + android:layout_gravity="right" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> </TableRow> <TableRow @@ -127,13 +133,16 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/server_info_pep" - android:textColor="@color/primarytext" /> + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> <TextView android:id="@+id/server_info_pep" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" /> + android:layout_gravity="right" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> </TableRow> <TableRow @@ -143,13 +152,17 @@ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/server_info_stream_management" /> + android:text="@string/server_info_stream_management" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> <TextView android:id="@+id/server_info_sm" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" /> + android:layout_gravity="right" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> </TableRow> <TableRow @@ -159,59 +172,64 @@ <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" - android:text="@string/server_info_carbon_messages" /> + android:text="@string/server_info_carbon_messages" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> <TextView android:id="@+id/server_info_carbons" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="right" /> + android:layout_gravity="right" + android:textColor="@color/primarytext" + android:textSize="?attr/TextSizeBody" /> </TableRow> </TableLayout> - <TextView - android:id="@+id/otr_fingerprint_headline" - android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:gravity="center_horizontal" - android:text="@string/otr_fingerprint" - android:textColor="@color/secondarytext" - android:textSize="?attr/TextSizeHeadline" - android:textStyle="bold" /> + <RelativeLayout android:layout_width="wrap_content" android:layout_height="match_parent" - android:layout_marginTop="8dp"> + android:id="@+id/otr_fingerprint_box" + android:layout_marginTop="32dp"> + <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_toLeftOf="@+id/action_copy_to_clipboard" - android:orientation="vertical" - android:layout_centerVertical="true"> + android:orientation="vertical"> <TextView android:id="@+id/otr_fingerprint" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:textColor="@color/primarytext" android:textSize="?attr/TextSizeBody" android:typeface="monospace" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" + android:text="@string/otr_fingerprint"/> </LinearLayout> <ImageButton android:id="@+id/action_copy_to_clipboard" - android:layout_width="32dp" - android:layout_height="32dp" - android:background="?android:selectableItemBackground" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" - android:padding="4dp" - android:scaleType="fitXY" + android:background="?android:selectableItemBackground" + android:padding="8dp" android:src="@drawable/ic_action_copy" - android:visibility="invisible" /> + android:visibility="visible" /> </RelativeLayout> + + </LinearLayout> </LinearLayout> </ScrollView> @@ -251,4 +269,4 @@ android:textColor="@color/secondarytext" /> </LinearLayout> -</RelativeLayout> +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/activity_muc_details.xml b/res/layout/activity_muc_details.xml index 1a676548..f689f10d 100644 --- a/res/layout/activity_muc_details.xml +++ b/res/layout/activity_muc_details.xml @@ -1,46 +1,35 @@ <?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@color/primarybackground" > + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:background="@color/secondarybackground" > <LinearLayout - android:layout_width="wrap_content" + android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" > - <TextView - style="@style/sectionHeader" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:padding="8dp" - android:text="@string/muc_details_conference" /> + <LinearLayout + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="16dp" > <TextView android:id="@+id/muc_jabberid" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:padding="8dp" - android:singleLine="true" android:text="@string/account_settings_example_jabber_id" android:textColor="@color/primarytext" - android:textSize="?attr/TextSizeBody" /> - - <TextView - style="@style/sectionHeader" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingTop="8dp" - android:text="@string/you" /> - + android:textSize="?attr/TextSizeHeadline" + android:textStyle="bold" + android:layout_marginBottom="16dp"/> + <RelativeLayout android:layout_width="fill_parent" - android:layout_height="wrap_content" - android:background="?android:attr/activatedBackgroundIndicator" - android:padding="8dp" - android:paddingBottom="8dp" > + android:layout_height="wrap_content"> <ImageView android:id="@+id/your_photo" @@ -85,22 +74,26 @@ android:padding="8dp" android:src="@drawable/ic_action_edit_dark" /> </RelativeLayout> - - <LinearLayout - android:id="@+id/muc_more_details" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:orientation="vertical" > - - <TextView - android:id="@+id/muc_participants_header" - style="@style/sectionHeader" + <TextView + android:id="@+id/details_account" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:paddingLeft="8dp" - android:paddingRight="8dp" - android:paddingTop="8dp" - android:text="@string/muc_details_other_members" /> + android:layout_gravity="right" + android:layout_marginTop="32dp" + android:text="@string/using_account" + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/muc_more_details" + android:layout_width="fill_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:background="@drawable/infocard_border" + android:orientation="vertical" + android:padding="8dp" > + <LinearLayout android:id="@+id/muc_members" @@ -111,7 +104,6 @@ android:orientation="vertical" android:showDividers="middle" > </LinearLayout> - </LinearLayout> <Button android:id="@+id/invite" @@ -123,4 +115,5 @@ android:text="@string/invite_contact" /> </LinearLayout> +</LinearLayout> </ScrollView>
\ No newline at end of file diff --git a/res/layout/contact_key.xml b/res/layout/contact_key.xml index e10f8420..7053857f 100644 --- a/res/layout/contact_key.xml +++ b/res/layout/contact_key.xml @@ -16,14 +16,15 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/primarytext" - android:textSize="?attr/TextSizeHeadline" + android:textSize="?attr/TextSizeBody" android:typeface="monospace" /> <TextView android:id="@+id/key_type" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/secondarytext" /> + android:textColor="@color/secondarytext" + android:textSize="?attr/TextSizeInfo"/> </LinearLayout> <ImageButton diff --git a/res/layout/message_received.xml b/res/layout/message_received.xml index 730d00d5..39bb842a 100644 --- a/res/layout/message_received.xml +++ b/res/layout/message_received.xml @@ -15,7 +15,8 @@ android:layout_alignParentBottom="true" android:layout_toRightOf="@+id/message_photo" android:background="@drawable/message_border" - android:minHeight="48dp" > + android:minHeight="48dp" + android:longClickable="true"> <LinearLayout android:layout_width="wrap_content" @@ -43,7 +44,6 @@ android:layout_height="wrap_content" android:autoLink="web" android:textColor="@color/primarytext" - android:textIsSelectable="true" android:textSize="?attr/TextSizeBody" /> <Button diff --git a/res/layout/message_sent.xml b/res/layout/message_sent.xml index 28f3ddc6..3e854643 100644 --- a/res/layout/message_sent.xml +++ b/res/layout/message_sent.xml @@ -15,7 +15,8 @@ android:layout_alignParentBottom="true" android:layout_toLeftOf="@+id/message_photo" android:background="@drawable/message_border" - android:minHeight="48dp" > + android:minHeight="48dp" + android:longClickable="true"> <LinearLayout android:layout_width="wrap_content" @@ -43,8 +44,15 @@ android:layout_height="wrap_content" android:autoLink="web" android:textColor="@color/primarytext" - android:textIsSelectable="true" android:textSize="?attr/TextSizeBody" /> + + <Button + android:id="@+id/download_button" + style="?android:attr/buttonStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/download_image" + android:visibility="gone" /> <LinearLayout android:layout_width="wrap_content" diff --git a/res/menu/message_context.xml b/res/menu/message_context.xml new file mode 100644 index 00000000..80d4d196 --- /dev/null +++ b/res/menu/message_context.xml @@ -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>
\ No newline at end of file diff --git a/res/values-de/arrays.xml b/res/values-de/arrays.xml index 9b429c5a..ed5d47b5 100644 --- a/res/values-de/arrays.xml +++ b/res/values-de/arrays.xml @@ -22,7 +22,7 @@ </string-array> <string-array name="mute_options_descriptions"> <item>30 Minuten</item> - <item>eine Stunde</item> + <item>1 Stunde</item> <item>2 Stunden</item> <item>8 Stunden</item> <item>bis auf Widerruf</item> diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 72121774..ca190deb 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -51,7 +51,7 @@ <string name="send_now">Jetzt abschicken</string> <string name="send_never">Nie mehr nachfragen</string> <string name="problem_connecting_to_account">Es gibt Probleme beim Verbindungsaufbau mit einem Konto</string> - <string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Konto</string> + <string name="problem_connecting_to_accounts">Es gibt Probleme beim Verbindungsaufbau mit mehreren Konten</string> <string name="touch_to_fix">Drücke hier, um das Konto zu verwalten</string> <string name="attach_file">Datei anfügen</string> <string name="not_in_roster">Der Kontakt ist nicht in deiner Kontaktliste. Möchtest du ihn hinzufügen?</string> @@ -65,7 +65,7 @@ <string name="clear_histor_msg">Möchtest du alle Nachrichten in dieser Unterhaltung löschen?\n\n<b>Achtung:</b> Dies beeinflusst nicht Nachrichten, die auf anderen Geräten oder Servern gespeichert sind.</string> <string name="delete_messages">Nachrichten löschen</string> <string name="also_end_conversation">Diese Unterhaltung danach beenden</string> - <string name="choose_presence">Choose presence to contact</string> + <string name="choose_presence">Ressource des Kontakts auswählen</string> <string name="send_plain_text_message">Unverschlüsselt schreiben</string> <string name="send_otr_message">OTR-verschlüsselt schreiben</string> <string name="send_pgp_message">OpenPGP-verschlüsselt schreiben</string> @@ -81,9 +81,9 @@ <string name="offering">angeboten…</string> <string name="waiting">warten…</string> <string name="no_pgp_key">Kein OpenPGP-Schlüssel gefunden</string> - <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string> + <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge OpenPGP einrichten.</small></string> <string name="no_pgp_keys">Keine OpenPGP-Schlüssel gefunden</string> - <string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string> + <string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil deine Kontakte ihre Schlüssel nicht preisgeben.\n\n<small>Bitte sag deinen Kontakten, sie mögen OpenPGP einrichten.</small></string> <string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier, um sie anzuzeigen und zu entschlüsseln.</i></string> <string name="encrypted_image_received"><i>Verschlüsseltes Bild erhalten. Drücke hier, um es anzuzeigen und zu entschlüsseln.</i></string> <string name="image_file"><i>Bild erhalten. Drücke hier, um es anzuzeigen.</i></string> @@ -100,7 +100,7 @@ <string name="pref_sound">Klingelton</string> <string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</string> <string name="pref_conference_notifications">Konferenz-Benachrichtigungen</string> - <string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde.</string> + <string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde</string> <string name="pref_notification_grace_period">Gnadenfrist</string> <string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.</string> <string name="pref_advanced_options">Erweiterte Optionen</string> @@ -233,7 +233,7 @@ <string name="skip">Überspringen</string> <string name="pref_ui_options">Benutzeroberfläche</string> <string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string> - <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte das dies nicht unbedingt in allen Fällen funktioniert.</string> + <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string> <string name="disable_notifications">Benachrichtigungen deaktivieren</string> <string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string> <string name="notifications_disabled">Benachrichtigungen sind deaktiviert</string> @@ -252,9 +252,11 @@ <string name="pref_force_encryption_summary">Nachrichten immer verschlüsseln (außer für Konferenzen)</string> <string name="pref_dont_save_encrypted">Verschlüsselte Nachrichten nicht speichern</string> <string name="pref_dont_save_encrypted_summary">Achtung: Kann zu Nachrichtenverlust führen</string> + <string name="pref_enable_legacy_ssl">Alte SSL-Version aktivieren</string> + <string name="pref_enable_legacy_ssl_summary">Aktiviert SSLv3-Unterstützung für alte Server. Achtung: SSLv3 ist unsicher.</string> <string name="pref_expert_options">Einstellungen für Experten</string> <string name="pref_expert_options_summary">Hier bitte vorsichtig sein</string> - <string name="pref_use_larger_font">Schriftgröße erhöhen</string> + <string name="pref_use_larger_font">Schrift vergrößern</string> <string name="pref_use_larger_font_summary">Überall in der App eine größere Schrift verwenden</string> <string name="pref_use_send_button_to_indicate_status">Absende-Knopf zeigt Online-Status an</string> <string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string> @@ -265,5 +267,19 @@ <string name="conference_banned">Du wurdest aus dem Konferenzraum verbannt</string> <string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</string> <string name="conference_kicked">Du wurdest aus dem Konferenzraum geworfen</string> + <string name="using_account">Verwende Konto %s</string> + <string name="checking_image">Prüfe Bild auf HTTP-Host</string> + <string name="image_file_deleted">Bilddatei wurde gelöscht</string> + <string name="not_connected_try_again">Nicht verbunden, bitte später versuchen</string> + <string name="check_image_filesize">Bildgröße prüfen</string> + <string name="message_options">Nachrichtenoptionen</string> + <string name="copy_text">Text kopieren</string> + <string name="share_image">Bild teilen</string> + <string name="copy_original_url">Original-URL kopieren</string> + <string name="send_again">Erneut senden</string> + <string name="image_url">Bild-URL</string> + <string name="message_text">Nachrichtentext</string> + <string name="url_copied_to_clipboard">URL in Zwischenablage kopiert</string> + <string name="message_copied_to_clipboard">Nachricht in Zwischenablage kopiert</string> </resources> diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 7fdc95c0..47424d00 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -250,6 +250,8 @@ <string name="pref_force_encryption_summary">Siempre enviar mensajes encriptados (excepto para conferencias)</string> <string name="pref_dont_save_encrypted">No guardar mensajes encriptados</string> <string name="pref_dont_save_encrypted_summary">Aviso: Esto podría llevar a pérdida de mensajes</string> + <string name="pref_enable_legacy_ssl">Habilitar SSL heredado</string> + <string name="pref_enable_legacy_ssl_summary">Habilita soporte SSLv3 para servidores heredados. Advertencia: SSLv3 se considera no seguro.</string> <string name="pref_expert_options">Ajustes avanzados</string> <string name="pref_expert_options_summary">Por favor, cuidado con estas opciones</string> <string name="pref_use_larger_font">Incrementar tamaño de fuente</string> @@ -265,5 +267,19 @@ <string name="conference_banned">Tu entrada a esta conferencia ha sido prohibida</string> <string name="conference_members_only">Esta conferencia es solo para miembros</string> <string name="conference_kicked">Has sido expulsado de esta conferencia</string> + <string name="using_account">Usando cuenta %s</string> + <string name="checking_image">Comprobando imagen en servidor HTTP</string> + <string name="image_file_deleted">El archivo de imagen ha sido eliminado</string> + <string name="not_connected_try_again">No estás concectado. Inténtalo más tarde</string> + <string name="check_image_filesize">Comprobar el tamaño del archivo de imagen</string> + <string name="message_options">Opciones de mensaje</string> + <string name="copy_text">Copiar texto</string> + <string name="share_image">Compartir imagen</string> + <string name="copy_original_url">Copiar URL original</string> + <string name="send_again">Volver a enviar</string> + <string name="image_url">URL Imagen</string> + <string name="message_text">Mensaje de texto</string> + <string name="url_copied_to_clipboard">URL copiada al portapapeles</string> + <string name="message_copied_to_clipboard">Mensaje copiado al portapapeles</string> </resources>
\ No newline at end of file diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index 4d3522fd..8896e9b5 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -205,7 +205,7 @@ <string name="conference_address">Konferentziaren helbidea</string> <string name="conference_address_example">gela@conference.example.com</string> <string name="save_as_bookmark">Gorde laster-marka bezala</string> - <string name="delete_bookmark">Laster-marka ezbatu</string> + <string name="delete_bookmark">Laster-marka ezabatu</string> <string name="bookmark_already_exists">Laster-marka hau existitzen da dagoeneko</string> <string name="you">Zu</string> <string name="action_edit_subject">Konferentziaren gaia editatu</string> @@ -250,11 +250,27 @@ <string name="pref_force_encryption_summary">Mezuak beti enkriptatuta bidali (konferentzietan izan ezik)</string> <string name="pref_dont_save_encrypted">Ez gorde enkriptatutako mezuak</string> <string name="pref_dont_save_encrypted_summary">Adi: Honek mezuen galera ekar lezake</string> + <string name="pref_enable_legacy_ssl">Oinordetutako SSL gaitu</string> + <string name="pref_enable_legacy_ssl_summary">SSLv3 gaitzen du oinordetutako zerbitzarietarako. Adi: SSLv3 ez segurutzat hartzen da.</string> <string name="pref_expert_options">Adituentzako aukerak</string> <string name="pref_expert_options_summary">Mesedez kontuz ibili hauekin</string> <string name="pref_use_larger_font">Letraren tamaina handitu</string> <string name="pref_use_larger_font_summary">Letra tamaina handiagoa erabili aplikazio osoan zehar</string> <string name="pref_use_send_button_to_indicate_status">Bidaltze botoiak egoera adierazten du</string> + <string name="pref_use_indicate_received">Mezuen jasotzea eskatu</string> + <string name="pref_use_indicate_received_summary">Jasotako mezuak marka berde batekin markatuko dira. Baliteke kasu guztietan ez funtzionatzea.</string> <string name="pref_use_send_button_to_indicate_status_summary">Bidaltze botoia koloreztatu kontaktu baten egoera adierazteko</string> + <string name="pref_expert_options_other">Besteak</string> + <string name="pref_conference_name">Konferentziaren izena</string> + <string name="pref_conference_name_summary">Erabili gelaren gaia konferentziak identifikatzeko eta ez JIDa</string> + <string name="toast_message_otr_fingerprint">OTR hatz-marka arbelara kopiatu da</string> + <string name="conference_banned">Konferentzia honetara sartzea debekatuta duzu</string> + <string name="conference_members_only">Konferentzia hau kideentzat da soilik</string> + <string name="conference_kicked">Konferentzia honetatik kanporatua izan zara</string> + <string name="using_account">%s kontua erabiltzen</string> + <string name="checking_image">Irudia egiaztatzen HTTP ostalarian</string> + <string name="image_file_deleted">Irudia ezabatu egin da</string> + <string name="not_connected_try_again">Ez zaude konektatuta. Saiatu beranduago berriz</string> + <string name="check_image_filesize">Irudiaren tamaina egiaztatu</string> -</resources>
\ No newline at end of file +</resources> diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index e15ef416..e1db316d 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -87,6 +87,7 @@ <string name="encrypted_message_received"><i>Message chiffré reçu. Appuyez pour le déchiffrer.</i></string> <string name="encrypted_image_received"><i>Image chiffrée reçue. Appuyez pour la déchiffrer.</i></string> <string name="image_file"><i>Image reçue. Appuyez pour visualiser.</i></string> + <string name="pref_general">Général</string> <string name="pref_xmpp_resource">Ressource XMPP</string> <string name="pref_xmpp_resource_summary">Nom permettant d\'identifier ce client XMPP</string> <string name="pref_accept_files">Accepter les fichiers</string> @@ -107,6 +108,7 @@ <string name="pref_never_send_crash_summary">En envoyant des logs vous aidez au développement de Conversations.</string> <string name="pref_confirm_messages">Confirmation de lecture</string> <string name="pref_confirm_messages_summary">Informer l\'expéditeur d\'un message de sa bonne réception.</string> + <string name="pref_ui_options">Options d\'affichage</string> <string name="openpgp_error">Une erreur s\'est produite via OpenKeychain</string> <string name="error_decrypting_file">Erreur d\'E/S lors du déchiffrement du fichier</string> <string name="accept">Accepter</string> @@ -145,6 +147,8 @@ <string name="mgmt_account_edit">Modifier le compte</string> <string name="mgmt_account_delete">Supprimer</string> <string name="mgmt_account_disable">Désactiver temporairement</string> + <string name="mgmt_account_publish_avatar">Publier un avatar</string> + <string name="mgmt_account_publish_pgp">Publier la clef publique OpenPGP</string> <string name="mgmt_account_enable">Activer</string> <string name="mgmt_account_are_you_sure">Êtes-vous sûr?</string> <string name="mgmt_account_delete_confirm_text">En supprimant votre compte, votre historique de conversations sera perdu!</string> @@ -169,6 +173,9 @@ <string name="muc_details_other_members">Autres membres</string> <string name="server_info_carbon_messages">Copies carbone</string> <string name="server_info_stream_management">Gestion des flux</string> + <string name="server_info_pep">XEP-0163: PEP (Avatars)</string> + <string name="server_info_available">disponible</string> + <string name="server_info_unavailable">indisponible</string> <string name="missing_public_keys">Aucune annonce de clef publique</string> <string name="last_seen_now">en ligne à l\'instant</string> <string name="last_seen_min">en ligne il y a 1 minute</string> @@ -207,6 +214,60 @@ <string name="contact_added_you">Votre correspondant vous a ajouté dans sa liste de contacts</string> <string name="add_back">Ajouter également</string> <string name="contact_has_read_up_to_this_point">%s a lu les messages précédents.</string> - <string name="pref_ui_options">Options d\'affichage</string> + <string name="publish">Publier</string> + <string name="touch_to_choose_picture">Toucher l\'avatar pour choisir une image depuis la galerie.</string> + <string name="publish_avatar_explanation">Nota Bene: Les personnes ayant activé les mises jour de présence verront cette image.</string> + <string name="publishing">Mise à jour…</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>
\ No newline at end of file +</resources> diff --git a/res/values-nl/styles.xml b/res/values-nl/styles.xml deleted file mode 100644 index 1468283e..00000000 --- a/res/values-nl/styles.xml +++ /dev/null @@ -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>
\ No newline at end of file diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 1a4fd25d..4acc9e62 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -22,7 +22,7 @@ </string-array> <string-array name="mute_options_descriptions"> <item>30 minutes</item> - <item>one hour</item> + <item>1 hour</item> <item>2 hours</item> <item>8 hours</item> <item>until further notice</item> @@ -36,4 +36,4 @@ <item>-1</item> </integer-array> -</resources>
\ No newline at end of file +</resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index 58af226b..e941ed6d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -250,13 +250,15 @@ <string name="pref_force_encryption_summary">Always send messages encrypted (except for conferences)</string> <string name="pref_dont_save_encrypted">Don’t save encrypted messages</string> <string name="pref_dont_save_encrypted_summary">Warning: This could lead to message loss</string> + <string name="pref_enable_legacy_ssl">Enable legacy SSL</string> + <string name="pref_enable_legacy_ssl_summary">Enables SSLv3 support for legacy servers. Warning: SSLv3 is considered insecure.</string> <string name="pref_expert_options">Expert options</string> - <string name="pref_expert_options_summary">Please be very careful with those</string> + <string name="pref_expert_options_summary">Please be careful with these</string> <string name="pref_use_larger_font">Increase font size</string> <string name="pref_use_larger_font_summary">Use larger font sizes across the entire app</string> <string name="pref_use_send_button_to_indicate_status">Send button indicates status</string> <string name="pref_use_indicate_received">Request message receipts</string> - <string name="pref_use_indicate_received_summary">Received masseges will be marked with a green tick. Be aware that this might no work in every case.</string> + <string name="pref_use_indicate_received_summary">Received messages will be marked with a green tick if supported</string> <string name="pref_use_send_button_to_indicate_status_summary">Colorize send button to indicate contact status</string> <string name="pref_expert_options_other">Other</string> <string name="pref_conference_name">Conference name</string> @@ -265,5 +267,20 @@ <string name="conference_banned">You are banned from this conference</string> <string name="conference_members_only">This conference is members only</string> <string name="conference_kicked">You have been kicked from this conference</string> + <string name="using_account">using account %s</string> + <string name="checking_image">Checking image on HTTP host</string> + <string name="image_file_deleted">The image file has been deleted</string> + <string name="not_connected_try_again">You are not connected. Try again later</string> + <string name="check_image_filesize">Check image file size</string> + <string name="message_options">Message options</string> + <string name="copy_text">Copy text</string> + <string name="share_image">Share image</string> + <string name="copy_original_url">Copy original URL</string> + <string name="send_again">Send again</string> + <string name="image_url">Image URL</string> + <string name="message_text">Message text</string> + <string name="url_copied_to_clipboard">URL copied to clipboard</string> + <string name="message_copied_to_clipboard">Message copied to clipboard</string> + <string name="image_transmission_failed">Image transmission failed</string> -</resources> +</resources>
\ No newline at end of file diff --git a/res/values/styles.xml b/res/values/styles.xml index a827fe36..64bde770 100644 --- a/res/values/styles.xml +++ b/res/values/styles.xml @@ -1,15 +1,4 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android"> - - <style name="sectionHeader" parent="android:Widget.Holo.Light.TextView"> - <item name="android:drawableBottom">@drawable/section_header</item> - <item name="android:drawablePadding">4dp</item> - <item name="android:layout_marginTop">8dp</item> - <item name="android:textSize">14sp</item> - <item name="android:textAllCaps">true</item> - <item name="android:textColor">@color/primarytext</item> - <item name="android:textStyle">bold</item> - </style> - <style name="Divider"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">1.5dp</item> diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index eccc8bae..06ab7560 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -51,6 +51,7 @@ android:title="@string/pref_sound" /> <CheckBoxPreference + android:defaultValue="true" android:dependency="show_notification" android:key="always_notify_in_conference" android:summary="@string/pref_conference_notifications_summary" @@ -88,6 +89,11 @@ android:key="dont_save_encrypted" android:summary="@string/pref_dont_save_encrypted_summary" android:title="@string/pref_dont_save_encrypted" /> + <CheckBoxPreference + android:defaultValue="false" + android:key="enable_legacy_ssl" + android:summary="@string/pref_enable_legacy_ssl_summary" + android:title="@string/pref_enable_legacy_ssl" /> </PreferenceCategory> <PreferenceCategory android:title="@string/pref_expert_options_other" > <CheckBoxPreference @@ -105,4 +111,4 @@ android:title="@string/pref_never_send_crash" /> </PreferenceCategory> -</PreferenceScreen>
\ No newline at end of file +</PreferenceScreen> diff --git a/src/eu/siacs/conversations/Config.java b/src/eu/siacs/conversations/Config.java index a11e1763..7dd5a799 100644 --- a/src/eu/siacs/conversations/Config.java +++ b/src/eu/siacs/conversations/Config.java @@ -10,7 +10,8 @@ public final class Config { public static final int PING_MIN_INTERVAL = 30; public static final int PING_TIMEOUT = 10; public static final int CONNECT_TIMEOUT = 90; - public static final int CARBON_GRACE_PERIOD = 120; + public static final int CARBON_GRACE_PERIOD = 60; + public static final int MINI_GRACE_PERIOD = 750; public static final int AVATAR_SIZE = 192; public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.WEBP; diff --git a/src/eu/siacs/conversations/crypto/PgpEngine.java b/src/eu/siacs/conversations/crypto/PgpEngine.java index e7058a68..9a2b4a11 100644 --- a/src/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/eu/siacs/conversations/crypto/PgpEngine.java @@ -8,25 +8,24 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URL; -import org.openintents.openpgp.OpenPgpError; import org.openintents.openpgp.OpenPgpSignatureResult; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpApi.IOpenPgpCallback; -import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.UiCallback; -import eu.siacs.conversations.xmpp.jingle.JingleFile; import android.app.PendingIntent; import android.content.Intent; import android.graphics.BitmapFactory; -import android.util.Log; +import android.net.Uri; public class PgpEngine { private OpenPgpApi api; @@ -39,7 +38,6 @@ public class PgpEngine { public void decrypt(final Message message, final UiCallback<Message> callback) { - Log.d(Config.LOGTAG, "decrypting message " + message.getUuid()); Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); params.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, message @@ -60,6 +58,10 @@ public class PgpEngine { if (message.getEncryption() == Message.ENCRYPTION_PGP) { message.setBody(os.toString()); message.setEncryption(Message.ENCRYPTION_DECRYPTED); + if (message.trusted() && message.bodyContainsDownloadable()) { + mXmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + } callback.success(message); } } catch (IOException e) { @@ -74,9 +76,6 @@ public class PgpEngine { message); return; case OpenPgpApi.RESULT_CODE_ERROR: - OpenPgpError error = result - .getParcelableExtra(OpenPgpApi.RESULT_ERROR); - Log.d(Config.LOGTAG, error.getMessage()); callback.error(R.string.openpgp_error, message); return; default: @@ -86,10 +85,10 @@ public class PgpEngine { }); } else if (message.getType() == Message.TYPE_IMAGE) { try { - final JingleFile inputFile = this.mXmppConnectionService - .getFileBackend().getJingleFile(message, false); - final JingleFile outputFile = this.mXmppConnectionService - .getFileBackend().getJingleFile(message, true); + final DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); + final DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); OutputStream os = new FileOutputStream(outputFile); @@ -100,19 +99,32 @@ public class PgpEngine { switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: + URL url = message.getImageParams().url; BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile( outputFile.getAbsolutePath(), options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; - message.setBody(Long.toString(outputFile.getSize()) - + ',' + imageWidth + ',' + imageHeight); + if (url == null) { + message.setBody(Long.toString(outputFile + .getSize()) + + '|' + + imageWidth + + '|' + + imageHeight); + } else { + message.setBody(url.toString() + "|" + + Long.toString(outputFile.getSize()) + + '|' + imageWidth + '|' + imageHeight); + } message.setEncryption(Message.ENCRYPTION_DECRYPTED); PgpEngine.this.mXmppConnectionService .updateMessage(message); - PgpEngine.this.mXmppConnectionService - .updateConversationUi(); + inputFile.delete(); + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + intent.setData(Uri.fromFile(outputFile)); + mXmppConnectionService.sendBroadcast(intent); callback.success(message); return; case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: @@ -197,10 +209,10 @@ public class PgpEngine { }); } else if (message.getType() == Message.TYPE_IMAGE) { try { - JingleFile inputFile = this.mXmppConnectionService - .getFileBackend().getJingleFile(message, true); - JingleFile outputFile = this.mXmppConnectionService - .getFileBackend().getJingleFile(message, false); + DownloadableFile inputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, true); + DownloadableFile outputFile = this.mXmppConnectionService + .getFileBackend().getFile(message, false); outputFile.createNewFile(); InputStream is = new FileInputStream(inputFile); OutputStream os = new FileOutputStream(outputFile); @@ -226,9 +238,11 @@ public class PgpEngine { } }); } catch (FileNotFoundException e) { - Log.d(Config.LOGTAG, "file not found: " + e.getMessage()); + callback.error(R.string.openpgp_error, message); + return; } catch (IOException e) { - Log.d(Config.LOGTAG, "io exception during file encrypt"); + callback.error(R.string.openpgp_error, message); + return; } } } @@ -272,11 +286,6 @@ public class PgpEngine { case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: return 0; case OpenPgpApi.RESULT_CODE_ERROR: - Log.d(Config.LOGTAG, - "openpgp error: " - + ((OpenPgpError) result - .getParcelableExtra(OpenPgpApi.RESULT_ERROR)) - .getMessage()); return 0; } return 0; diff --git a/src/eu/siacs/conversations/entities/Account.java b/src/eu/siacs/conversations/entities/Account.java index eacd172c..80a9d62f 100644 --- a/src/eu/siacs/conversations/entities/Account.java +++ b/src/eu/siacs/conversations/entities/Account.java @@ -11,16 +11,14 @@ import net.java.otr4j.crypto.OtrCryptoException; import org.json.JSONException; import org.json.JSONObject; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.OtrEngine; -import eu.siacs.conversations.persistance.FileBackend; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.XmppConnection; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; -import android.graphics.Bitmap; +import android.os.SystemClock; public class Account extends AbstractEntity { @@ -64,16 +62,14 @@ public class Account extends AbstractEntity { protected boolean online = false; - transient OtrEngine otrEngine = null; - transient XmppConnection xmppConnection = null; - transient protected Presences presences = new Presences(); - + private OtrEngine otrEngine = null; + private XmppConnection xmppConnection = null; + private Presences presences = new Presences(); + private long mEndGracePeriod = 0L; private String otrFingerprint; - private Roster roster = null; private List<Bookmark> bookmarks = new CopyOnWriteArrayList<Bookmark>(); - public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<Conversation>(); public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<Conversation>(); @@ -160,8 +156,12 @@ public class Account extends AbstractEntity { } public boolean hasErrorStatus() { - return getStatus() > STATUS_NO_INTERNET - && (getXmppConnection().getAttempt() >= 2); + if (getXmppConnection() == null) { + return false; + } else { + return getStatus() > STATUS_NO_INTERNET + && (getXmppConnection().getAttempt() >= 2); + } } public void setResource(String resource) { @@ -341,20 +341,6 @@ public class Account extends AbstractEntity { return false; } - public Bitmap getImage(Context context, int size) { - if (this.avatar != null) { - Bitmap bm = FileBackend.getAvatar(this.avatar, size, context); - if (bm == null) { - return UIHelper.getContactPicture(getJid(), size, context, - false); - } else { - return bm; - } - } else { - return UIHelper.getContactPicture(getJid(), size, context, false); - } - } - public boolean setAvatar(String filename) { if (this.avatar != null && this.avatar.equals(filename)) { return false; @@ -397,4 +383,17 @@ public class Account extends AbstractEntity { return R.string.account_status_unknown; } } + + public void activateGracePeriod() { + this.mEndGracePeriod = SystemClock.elapsedRealtime() + + (Config.CARBON_GRACE_PERIOD * 1000); + } + + public void deactivateGracePeriod() { + this.mEndGracePeriod = 0L; + } + + public boolean inGracePeriod() { + return SystemClock.elapsedRealtime() < this.mEndGracePeriod; + } } diff --git a/src/eu/siacs/conversations/entities/Bookmark.java b/src/eu/siacs/conversations/entities/Bookmark.java index 722fb6d9..dd9e805c 100644 --- a/src/eu/siacs/conversations/entities/Bookmark.java +++ b/src/eu/siacs/conversations/entities/Bookmark.java @@ -2,9 +2,6 @@ package eu.siacs.conversations.entities; import java.util.Locale; -import android.content.Context; -import android.graphics.Bitmap; -import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; public class Bookmark extends Element implements ListItem { @@ -120,21 +117,14 @@ public class Bookmark extends Element implements ListItem { return this.account; } - @Override - public Bitmap getImage(int dpSize, Context context) { - if (this.mJoinedConversation == null) { - return UIHelper.getContactPicture(getDisplayName(), dpSize, - context, false); - } else { - return UIHelper.getContactPicture(this.mJoinedConversation, dpSize, - context, false); - } - } - public void setConversation(Conversation conversation) { this.mJoinedConversation = conversation; } + public Conversation getConversation() { + return this.mJoinedConversation; + } + public String getName() { return this.getAttribute("name"); } diff --git a/src/eu/siacs/conversations/entities/Contact.java b/src/eu/siacs/conversations/entities/Contact.java index b1ebe662..60c31a42 100644 --- a/src/eu/siacs/conversations/entities/Contact.java +++ b/src/eu/siacs/conversations/entities/Contact.java @@ -8,13 +8,9 @@ import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import eu.siacs.conversations.persistance.FileBackend; -import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; -import android.graphics.Bitmap; public class Contact implements ListItem { public static final String TABLENAME = "contacts"; @@ -330,20 +326,6 @@ public class Contact implements ListItem { } } - @Override - public Bitmap getImage(int size, Context context) { - if (this.avatar != null) { - Bitmap bm = FileBackend.getAvatar(avatar, size, context); - if (bm == null) { - return UIHelper.getContactPicture(this, size, context, false); - } else { - return bm; - } - } else { - return UIHelper.getContactPicture(this, size, context, false); - } - } - public boolean setAvatar(String filename) { if (this.avatar != null && this.avatar.equals(filename)) { return false; @@ -353,6 +335,10 @@ public class Contact implements ListItem { } } + public String getAvatar() { + return this.avatar; + } + public boolean deleteOtrFingerprint(String fingerprint) { boolean success = false; try { @@ -374,4 +360,8 @@ public class Contact implements ListItem { return false; } } + + public boolean trusted() { + return getOption(Options.FROM) && getOption(Options.TO); + } } diff --git a/src/eu/siacs/conversations/entities/Conversation.java b/src/eu/siacs/conversations/entities/Conversation.java index b4c99dc1..9d4c36db 100644 --- a/src/eu/siacs/conversations/entities/Conversation.java +++ b/src/eu/siacs/conversations/entities/Conversation.java @@ -1,14 +1,13 @@ package eu.siacs.conversations.entities; import java.security.interfaces.DSAPublicKey; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; import org.json.JSONException; import org.json.JSONObject; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.utils.UIHelper; import net.java.otr4j.OtrException; import net.java.otr4j.crypto.OtrCryptoEngineImpl; @@ -17,9 +16,7 @@ import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionImpl; import net.java.otr4j.session.SessionStatus; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; -import android.graphics.Bitmap; import android.os.SystemClock; public class Conversation extends AbstractEntity { @@ -57,8 +54,8 @@ public class Conversation extends AbstractEntity { private String nextPresence; - private transient CopyOnWriteArrayList<Message> messages = null; - private transient Account account = null; + protected ArrayList<Message> messages = new ArrayList<Message>(); + protected Account account = null; private transient SessionImpl otrSession; @@ -68,12 +65,10 @@ public class Conversation extends AbstractEntity { private transient MucOptions mucOptions = null; - //private transient String latestMarkableMessageId; + // private transient String latestMarkableMessageId; private byte[] symmetricKey; - private boolean otrSessionNeedsStarting = false; - private Bookmark bookmark; public Conversation(String name, Account account, String contactJid, @@ -106,17 +101,6 @@ public class Conversation extends AbstractEntity { } public List<Message> getMessages() { - if (messages == null) { - this.messages = new CopyOnWriteArrayList<Message>(); // prevent null - // pointer - } - - // populate with Conversation (this) - - for (Message msg : messages) { - msg.setConversation(this); - } - return messages; } @@ -142,8 +126,9 @@ public class Conversation extends AbstractEntity { if (this.messages == null) { return null; } - for(int i = this.messages.size() - 1; i >= 0; --i) { - if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED && this.messages.get(i).markable) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED + && this.messages.get(i).markable) { if (this.messages.get(i).isRead()) { return null; } else { @@ -166,7 +151,7 @@ public class Conversation extends AbstractEntity { } } - public void setMessages(CopyOnWriteArrayList<Message> msgs) { + public void setMessages(ArrayList<Message> msgs) { this.messages = msgs; } @@ -263,10 +248,7 @@ public class Conversation extends AbstractEntity { try { if (sendStart) { this.otrSession.startSession(); - this.otrSessionNeedsStarting = false; return this.otrSession; - } else { - this.otrSessionNeedsStarting = true; } return this.otrSession; } catch (OtrException e) { @@ -282,12 +264,12 @@ public class Conversation extends AbstractEntity { public void resetOtrSession() { this.otrFingerprint = null; - this.otrSessionNeedsStarting = false; this.otrSession = null; } public void startOtrIfNeeded() { - if (this.otrSession != null && this.otrSessionNeedsStarting) { + if (this.otrSession != null + && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { try { this.otrSession.startSession(); } catch (OtrException e) { @@ -296,18 +278,23 @@ public class Conversation extends AbstractEntity { } } - public void endOtrIfNeeded() { + public boolean endOtrIfNeeded() { if (this.otrSession != null) { if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) { try { this.otrSession.endSession(); this.resetOtrSession(); + return true; } catch (OtrException e) { this.resetOtrSession(); + return false; } } else { this.resetOtrSession(); + return false; } + } else { + return false; } } @@ -339,9 +326,8 @@ public class Conversation extends AbstractEntity { public synchronized MucOptions getMucOptions() { if (this.mucOptions == null) { - this.mucOptions = new MucOptions(this.getAccount()); + this.mucOptions = new MucOptions(this); } - this.mucOptions.setConversation(this); return this.mucOptions; } @@ -438,14 +424,6 @@ public class Conversation extends AbstractEntity { return this.bookmark; } - public Bitmap getImage(Context context, int size) { - if (mode == MODE_SINGLE) { - return getContact().getImage(size, context); - } else { - return UIHelper.getContactPicture(this, size, context, false); - } - } - public boolean hasDuplicateMessage(Message message) { for (int i = this.getMessages().size() - 1; i >= 0; --i) { if (this.messages.get(i).equals(message)) { @@ -506,4 +484,17 @@ public class Conversation extends AbstractEntity { } } } + + public void add(Message message) { + message.setConversation(this); + synchronized (this.messages) { + this.messages.add(message); + } + } + + public void addAll(int index, List<Message> messages) { + synchronized (this.messages) { + this.messages.addAll(index, messages); + } + } } diff --git a/src/eu/siacs/conversations/entities/Downloadable.java b/src/eu/siacs/conversations/entities/Downloadable.java index 8fb4977e..70516b20 100644 --- a/src/eu/siacs/conversations/entities/Downloadable.java +++ b/src/eu/siacs/conversations/entities/Downloadable.java @@ -1,5 +1,21 @@ package eu.siacs.conversations.entities; public interface Downloadable { - public void start(); + + public final String[] VALID_EXTENSIONS = { "webp", "jpeg", "jpg", "png" }; + public final String[] VALID_CRYPTO_EXTENSIONS = { "pgp", "gpg", "otr" }; + + public static final int STATUS_UNKNOWN = 0x200; + public static final int STATUS_CHECKING = 0x201; + public static final int STATUS_FAILED = 0x202; + public static final int STATUS_OFFER = 0x203; + public static final int STATUS_DOWNLOADING = 0x204; + public static final int STATUS_DELETED = 0x205; + public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206; + + public boolean start(); + + public int getStatus(); + + public long getFileSize(); } diff --git a/src/eu/siacs/conversations/entities/DownloadableFile.java b/src/eu/siacs/conversations/entities/DownloadableFile.java new file mode 100644 index 00000000..1605c75b --- /dev/null +++ b/src/eu/siacs/conversations/entities/DownloadableFile.java @@ -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; + } + } + } +} diff --git a/src/eu/siacs/conversations/entities/ListItem.java b/src/eu/siacs/conversations/entities/ListItem.java index 19089b28..a1872d2f 100644 --- a/src/eu/siacs/conversations/entities/ListItem.java +++ b/src/eu/siacs/conversations/entities/ListItem.java @@ -1,12 +1,7 @@ package eu.siacs.conversations.entities; -import android.content.Context; -import android.graphics.Bitmap; - public interface ListItem extends Comparable<ListItem> { public String getDisplayName(); public String getJid(); - - public Bitmap getImage(int dpSize, Context context); } diff --git a/src/eu/siacs/conversations/entities/Message.java b/src/eu/siacs/conversations/entities/Message.java index 49482bbc..8a83c465 100644 --- a/src/eu/siacs/conversations/entities/Message.java +++ b/src/eu/siacs/conversations/entities/Message.java @@ -1,23 +1,21 @@ package eu.siacs.conversations.entities; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; public class Message extends AbstractEntity { public static final String TABLENAME = "messages"; - public static final int STATUS_RECEPTION_FAILED = -3; - public static final int STATUS_RECEIVED_OFFER = -2; - public static final int STATUS_RECEIVING = -1; public static final int STATUS_RECEIVED = 0; public static final int STATUS_UNSEND = 1; public static final int STATUS_SEND = 2; public static final int STATUS_SEND_FAILED = 3; - public static final int STATUS_SEND_REJECTED = 4; public static final int STATUS_WAITING = 5; public static final int STATUS_OFFERED = 6; public static final int STATUS_SEND_RECEIVED = 7; @@ -61,6 +59,9 @@ public class Message extends AbstractEntity { protected Downloadable downloadable = null; public boolean markable = false; + private Message mNextMessage = null; + private Message mPreviousMessage = null; + private Message() { } @@ -131,14 +132,8 @@ public class Message extends AbstractEntity { if (this.trueCounterpart == null) { return null; } else { - Account account = this.conversation.getAccount(); - Contact contact = account.getRoster().getContact( - this.trueCounterpart); - if (contact.showInRoster()) { - return contact; - } else { - return null; - } + return this.conversation.getAccount().getRoster() + .getContactFromRoster(this.trueCounterpart); } } } @@ -147,22 +142,6 @@ public class Message extends AbstractEntity { return body; } - public String getReadableBody(Context context) { - if ((encryption == ENCRYPTION_PGP) && (type == TYPE_TEXT)) { - return context.getText(R.string.encrypted_message_received) - .toString(); - } else if ((encryption == ENCRYPTION_OTR) && (type == TYPE_IMAGE)) { - return context.getText(R.string.encrypted_image_received) - .toString(); - } else if (encryption == ENCRYPTION_DECRYPTION_FAILED) { - return context.getText(R.string.decryption_failed).toString(); - } else if (type == TYPE_IMAGE) { - return context.getText(R.string.image_file).toString(); - } else { - return body.trim(); - } - } - public long getTimeSent() { return timeSent; } @@ -301,21 +280,34 @@ public class Message extends AbstractEntity { } public Message next() { - int index = this.conversation.getMessages().indexOf(this); - if (index < 0 || index >= this.conversation.getMessages().size() - 1) { - return null; - } else { - return this.conversation.getMessages().get(index + 1); + if (this.mNextMessage == null) { + synchronized (this.conversation.messages) { + int index = this.conversation.messages.indexOf(this); + if (index < 0 + || index >= this.conversation.getMessages().size() - 1) { + this.mNextMessage = null; + } else { + this.mNextMessage = this.conversation.messages + .get(index + 1); + } + } } + return this.mNextMessage; } public Message prev() { - int index = this.conversation.getMessages().indexOf(this); - if (index <= 0 || index > this.conversation.getMessages().size()) { - return null; - } else { - return this.conversation.getMessages().get(index - 1); + if (this.mPreviousMessage == null) { + synchronized (this.conversation.messages) { + int index = this.conversation.messages.indexOf(this); + if (index <= 0 || index > this.conversation.messages.size()) { + this.mPreviousMessage = null; + } else { + this.mPreviousMessage = this.conversation.messages + .get(index - 1); + } + } } + return this.mPreviousMessage; } public boolean mergable(Message message) { @@ -323,6 +315,8 @@ public class Message extends AbstractEntity { return false; } return (message.getType() == Message.TYPE_TEXT + && this.getDownloadable() == null + && message.getDownloadable() == null && message.getEncryption() != Message.ENCRYPTION_PGP && this.getType() == message.getType() && this.getEncryption() == message.getEncryption() @@ -332,7 +326,9 @@ public class Message extends AbstractEntity { .getStatus() == Message.STATUS_SEND_RECEIVED) && (message .getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND || message - .getStatus() == Message.STATUS_SEND_DISPLAYED))))); + .getStatus() == Message.STATUS_SEND_DISPLAYED)))) + && !message.bodyContainsDownloadable() + && !this.bodyContainsDownloadable()); } public String getMergedBody() { @@ -369,4 +365,148 @@ public class Message extends AbstractEntity { return prev.mergable(this); } } + + public boolean trusted() { + Contact contact = this.getContact(); + return (status > STATUS_RECEIVED || (contact != null && contact.trusted())); + } + + public boolean bodyContainsDownloadable() { + try { + URL url = new URL(this.getBody()); + if (!url.getProtocol().equalsIgnoreCase("http") + && !url.getProtocol().equalsIgnoreCase("https")) { + return false; + } + if (url.getPath() == null) { + return false; + } + String[] pathParts = url.getPath().split("/"); + String filename; + if (pathParts.length > 0) { + filename = pathParts[pathParts.length - 1]; + } else { + filename = pathParts[0]; + } + String[] extensionParts = filename.split("\\."); + if (extensionParts.length == 2 + && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( + extensionParts[extensionParts.length - 1])) { + return true; + } else if (extensionParts.length == 3 + && Arrays + .asList(Downloadable.VALID_CRYPTO_EXTENSIONS) + .contains(extensionParts[extensionParts.length - 1]) + && Arrays.asList(Downloadable.VALID_EXTENSIONS).contains( + extensionParts[extensionParts.length - 2])) { + return true; + } else { + return false; + } + } catch (MalformedURLException e) { + return false; + } + } + + public ImageParams getImageParams() { + ImageParams params = getLegacyImageParams(); + if (params!=null) { + return params; + } + params = new ImageParams(); + if (this.downloadable != null) { + params.size = this.downloadable.getFileSize(); + } + if (body == null) { + return params; + } + String parts[] = body.split("\\|"); + if (parts.length == 1) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.origin = parts[0]; + try { + params.url = new URL(parts[0]); + } catch (MalformedURLException e1) { + params.url = null; + } + } + } else if (parts.length == 3) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.size = 0; + } + try { + params.width = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + params.height = 0; + } + } else if (parts.length == 4) { + params.origin = parts[0]; + try { + params.url = new URL(parts[0]); + } catch (MalformedURLException e1) { + params.url = null; + } + try { + params.size = Long.parseLong(parts[1]); + } catch (NumberFormatException e) { + params.size = 0; + } + try { + params.width = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + params.height = 0; + } + } + return params; + } + + public ImageParams getLegacyImageParams() { + ImageParams params = new ImageParams(); + if (body == null) { + return params; + } + String parts[] = body.split(","); + if (parts.length == 3) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + return null; + } + try { + params.width = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + return null; + } + try { + params.height = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + return null; + } + return params; + } else { + return null; + } + } + + public class ImageParams { + public URL url; + public long size = 0; + public int width = 0; + public int height = 0; + public String origin; + } } diff --git a/src/eu/siacs/conversations/entities/MucOptions.java b/src/eu/siacs/conversations/entities/MucOptions.java index 12ea4e93..d7407cd5 100644 --- a/src/eu/siacs/conversations/entities/MucOptions.java +++ b/src/eu/siacs/conversations/entities/MucOptions.java @@ -102,6 +102,10 @@ public class MucOptions { public long getPgpKeyId() { return this.pgpKeyId; } + + public Contact getContact() { + return account.getRoster().getContactFromRoster(getJid()); + } } private Account account; @@ -116,8 +120,9 @@ public class MucOptions { private String joinnick; private String password = null; - public MucOptions(Account account) { - this.account = account; + public MucOptions(Conversation conversation) { + this.account = conversation.getAccount(); + this.conversation = conversation; } public void deleteUser(String name) { @@ -253,10 +258,6 @@ public class MucOptions { this.joinnick = nick; } - public void setConversation(Conversation conversation) { - this.conversation = conversation; - } - public boolean online() { return this.isOnline; } @@ -361,4 +362,8 @@ public class MucOptions { conversation .setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password); } + + public Conversation getConversation() { + return this.conversation; + } }
\ No newline at end of file diff --git a/src/eu/siacs/conversations/entities/Roster.java b/src/eu/siacs/conversations/entities/Roster.java index b6908793..3267b15a 100644 --- a/src/eu/siacs/conversations/entities/Roster.java +++ b/src/eu/siacs/conversations/entities/Roster.java @@ -14,7 +14,10 @@ public class Roster { this.account = account; } - public Contact getContactAsShownInRoster(String jid) { + public Contact getContactFromRoster(String jid) { + if (jid == null) { + return null; + } String cleanJid = jid.split("/", 2)[0]; Contact contact = contacts.get(cleanJid); if (contact != null && contact.showInRoster()) { diff --git a/src/eu/siacs/conversations/http/HttpConnection.java b/src/eu/siacs/conversations/http/HttpConnection.java new file mode 100644 index 00000000..0810e167 --- /dev/null +++ b/src/eu/siacs/conversations/http/HttpConnection.java @@ -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; + } + } +}
\ No newline at end of file diff --git a/src/eu/siacs/conversations/http/HttpConnectionManager.java b/src/eu/siacs/conversations/http/HttpConnectionManager.java new file mode 100644 index 00000000..9a2a2405 --- /dev/null +++ b/src/eu/siacs/conversations/http/HttpConnectionManager.java @@ -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); + } +} diff --git a/src/eu/siacs/conversations/parser/MessageParser.java b/src/eu/siacs/conversations/parser/MessageParser.java index f8329037..383ac89a 100644 --- a/src/eu/siacs/conversations/parser/MessageParser.java +++ b/src/eu/siacs/conversations/parser/MessageParser.java @@ -256,7 +256,6 @@ public class MessageParser extends AbstractParser implements return null; } } - return finishedMessage; } @@ -348,10 +347,18 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.databaseBackend .updateAccount(account); } + mXmppConnectionService.getAvatarService().clear( + account); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateAccountUi(); } else { Contact contact = account.getRoster().getContact( from); contact.setAvatar(avatar.getFilename()); + mXmppConnectionService.getAvatarService().clear( + contact); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); } } else { mXmppConnectionService.fetchAvatar(account, avatar); @@ -417,8 +424,7 @@ public class MessageParser extends AbstractParser implements message = this.parseCarbonMessage(packet, account); if (message != null) { if (message.getStatus() == Message.STATUS_SEND) { - mXmppConnectionService.getNotificationService() - .activateGracePeriod(); + account.activateGracePeriod(); notify = false; mXmppConnectionService.markRead( message.getConversation(), false); @@ -440,8 +446,7 @@ public class MessageParser extends AbstractParser implements } else { mXmppConnectionService.markRead(message.getConversation(), false); - mXmppConnectionService.getNotificationService() - .activateGracePeriod(); + account.activateGracePeriod(); notify = false; } } @@ -471,13 +476,26 @@ public class MessageParser extends AbstractParser implements } } Conversation conversation = message.getConversation(); - conversation.getMessages().add(message); + conversation.add(message); + + if (message.getStatus() == Message.STATUS_RECEIVED + && conversation.getOtrSession() != null + && !conversation.getOtrSession().getSessionID().getUserID() + .equals(message.getPresence())) { + conversation.endOtrIfNeeded(); + } + if (packet.getType() != MessagePacket.TYPE_ERROR) { if (message.getEncryption() == Message.ENCRYPTION_NONE || mXmppConnectionService.saveEncryptedMessages()) { mXmppConnectionService.databaseBackend.createMessage(message); } } + if (message.trusted() && message.bodyContainsDownloadable()) { + this.mXmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + notify = false; + } notify = notify && !conversation.isMuted(); if (notify) { mXmppConnectionService.getNotificationService().push(message); diff --git a/src/eu/siacs/conversations/parser/PresenceParser.java b/src/eu/siacs/conversations/parser/PresenceParser.java index 2c3a7dbc..4e90cda8 100644 --- a/src/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/eu/siacs/conversations/parser/PresenceParser.java @@ -29,6 +29,7 @@ public class PresenceParser extends AbstractParser implements if (before != muc.getMucOptions().online()) { mXmppConnectionService.updateConversationUi(); } + mXmppConnectionService.getAvatarService().clear(muc); } } else if (packet.hasChild("x", "http://jabber.org/protocol/muc")) { Conversation muc = mXmppConnectionService.find(account, packet @@ -39,6 +40,7 @@ public class PresenceParser extends AbstractParser implements if (before != muc.getMucOptions().online()) { mXmppConnectionService.updateConversationUi(); } + mXmppConnectionService.getAvatarService().clear(muc); } } } @@ -58,8 +60,7 @@ public class PresenceParser extends AbstractParser implements Presences.parseShow(packet.findChild("show"))); } else if (type.equals("unavailable")) { account.removePresence(fromParts[1]); - mXmppConnectionService.getNotificationService() - .deactivateGracePeriod(); + account.deactivateGracePeriod(); } } } else { diff --git a/src/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/eu/siacs/conversations/persistance/DatabaseBackend.java index 0991dd2d..12e5e251 100644 --- a/src/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -11,6 +11,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Roster; import android.content.Context; import android.database.Cursor; +import android.database.sqlite.SQLiteCantOpenDatabaseException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; @@ -151,14 +152,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { return list; } - public CopyOnWriteArrayList<Message> getMessages( - Conversation conversations, int limit) { + public ArrayList<Message> getMessages(Conversation conversations, int limit) { return getMessages(conversations, limit, -1); } - public CopyOnWriteArrayList<Message> getMessages(Conversation conversation, - int limit, long timestamp) { - CopyOnWriteArrayList<Message> list = new CopyOnWriteArrayList<Message>(); + public ArrayList<Message> getMessages(Conversation conversation, int limit, + long timestamp) { + ArrayList<Message> list = new ArrayList<Message>(); SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; if (timestamp == -1) { @@ -177,7 +177,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (cursor.getCount() > 0) { cursor.moveToLast(); do { - list.add(Message.fromCursor(cursor)); + Message message = Message.fromCursor(cursor); + message.setConversation(conversation); + list.add(message); } while (cursor.moveToPrevious()); } return list; @@ -231,10 +233,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor = db.rawQuery("select count(" + Account.UUID + ") from " + Account.TABLENAME + " where not options & (1 <<1)", null); - cursor.moveToFirst(); - int count = cursor.getInt(0); - cursor.close(); - return (count > 0); + try { + cursor.moveToFirst(); + int count = cursor.getInt(0); + cursor.close(); + return (count > 0); + } catch (SQLiteCantOpenDatabaseException e) { + return true; // better safe than sorry + } } @Override @@ -326,4 +332,22 @@ public class DatabaseBackend extends SQLiteOpenHelper { cursor.moveToFirst(); return Account.fromCursor(cursor); } + + public List<Message> getImageMessages(Conversation conversation) { + ArrayList<Message> list = new ArrayList<Message>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor; + String[] selectionArgs = { conversation.getUuid(), String.valueOf(Message.TYPE_IMAGE) }; + cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION + + "=? AND "+Message.TYPE+"=?", selectionArgs, null, null,null); + if (cursor.getCount() > 0) { + cursor.moveToLast(); + do { + Message message = Message.fromCursor(cursor); + message.setConversation(conversation); + list.add(message); + } while (cursor.moveToPrevious()); + } + return list; + } } diff --git a/src/eu/siacs/conversations/persistance/FileBackend.java b/src/eu/siacs/conversations/persistance/FileBackend.java index d86c0ee1..13daa27b 100644 --- a/src/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/eu/siacs/conversations/persistance/FileBackend.java @@ -14,89 +14,47 @@ import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; -import android.content.Context; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Matrix; import android.graphics.RectF; -import android.media.ExifInterface; import android.net.Uri; import android.os.Environment; import android.provider.MediaStore; import android.util.Base64; import android.util.Base64OutputStream; import android.util.Log; -import android.util.LruCache; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.services.ImageProvider; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; -import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.xmpp.jingle.JingleFile; +import eu.siacs.conversations.utils.ExifHelper; import eu.siacs.conversations.xmpp.pep.Avatar; public class FileBackend { private static int IMAGE_SIZE = 1920; - private Context context; - private LruCache<String, Bitmap> thumbnailCache; - private SimpleDateFormat imageDateFormat = new SimpleDateFormat( "yyyyMMdd_HHmmssSSS", Locale.US); - public FileBackend(Context context) { - this.context = context; - int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - int cacheSize = maxMemory / 8; - thumbnailCache = new LruCache<String, Bitmap>(cacheSize) { - @Override - protected int sizeOf(String key, Bitmap bitmap) { - return bitmap.getByteCount() / 1024; - } - }; - - } - - public LruCache<String, Bitmap> getThumbnailCache() { - return thumbnailCache; - } + private XmppConnectionService mXmppConnectionService; - public JingleFile getJingleFileLegacy(Message message) { - return getJingleFileLegacy(message, true); - } - - public JingleFile getJingleFileLegacy(Message message, boolean decrypted) { - Conversation conversation = message.getConversation(); - String prefix = context.getFilesDir().getAbsolutePath(); - String path = prefix + "/" + conversation.getAccount().getJid() + "/" - + conversation.getContactJid(); - String filename; - if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { - filename = message.getUuid() + ".webp"; - } else { - if (message.getEncryption() == Message.ENCRYPTION_OTR) { - filename = message.getUuid() + ".webp"; - } else { - filename = message.getUuid() + ".webp.pgp"; - } - } - return new JingleFile(path + "/" + filename); + public FileBackend(XmppConnectionService service) { + this.mXmppConnectionService = service; } - public JingleFile getJingleFile(Message message) { - return getJingleFile(message, true); + public DownloadableFile getFile(Message message) { + return getFile(message, true); } - public JingleFile getJingleFile(Message message, boolean decrypted) { + public DownloadableFile getFile(Message message, boolean decrypted) { StringBuilder filename = new StringBuilder(); - filename.append(Environment.getExternalStoragePublicDirectory( - Environment.DIRECTORY_PICTURES).getAbsolutePath()); - filename.append("/Conversations/"); + filename.append(getConversationsDirectory()); filename.append(message.getUuid()); if ((decrypted) || (message.getEncryption() == Message.ENCRYPTION_NONE)) { filename.append(".webp"); @@ -107,7 +65,13 @@ public class FileBackend { filename.append(".webp.pgp"); } } - return new JingleFile(filename.toString()); + return new DownloadableFile(filename.toString()); + } + + public static String getConversationsDirectory() { + return Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_PICTURES).getAbsolutePath() + + "/Conversations/"; } public Bitmap resize(Bitmap originalBitmap, int size) { @@ -139,17 +103,17 @@ public class FileBackend { return Bitmap.createBitmap(bitmap, 0, 0, w, h, mtx, true); } - public JingleFile copyImageToPrivateStorage(Message message, Uri image) + public DownloadableFile copyImageToPrivateStorage(Message message, Uri image) throws ImageCopyException { return this.copyImageToPrivateStorage(message, image, 0); } - private JingleFile copyImageToPrivateStorage(Message message, Uri image, - int sampleSize) throws ImageCopyException { + private DownloadableFile copyImageToPrivateStorage(Message message, + Uri image, int sampleSize) throws ImageCopyException { try { - InputStream is = context.getContentResolver() + InputStream is = mXmppConnectionService.getContentResolver() .openInputStream(image); - JingleFile file = getJingleFile(message); + DownloadableFile file = getFile(message); file.getParentFile().mkdirs(); file.createNewFile(); Bitmap originalBitmap; @@ -202,7 +166,7 @@ public class FileBackend { private int getRotation(Uri image) { if ("content".equals(image.getScheme())) { try { - Cursor cursor = context + Cursor cursor = mXmppConnectionService .getContentResolver() .query(image, new String[] { MediaStore.Images.ImageColumns.ORIENTATION }, @@ -216,40 +180,26 @@ public class FileBackend { return -1; } } else { - ExifInterface exif; try { - exif = new ExifInterface(image.toString()); - if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) - .equalsIgnoreCase("6")) { - return 90; - } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) - .equalsIgnoreCase("8")) { - return 270; - } else if (exif.getAttribute(ExifInterface.TAG_ORIENTATION) - .equalsIgnoreCase("3")) { - return 180; - } else { - return 0; - } - } catch (IOException e) { - return -1; + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + return ExifHelper.getOrientation(is); + } catch (FileNotFoundException e) { + return 0; } } } public Bitmap getImageFromMessage(Message message) { - return BitmapFactory.decodeFile(getJingleFile(message) - .getAbsolutePath()); + return BitmapFactory.decodeFile(getFile(message).getAbsolutePath()); } public Bitmap getThumbnail(Message message, int size, boolean cacheOnly) throws FileNotFoundException { - Bitmap thumbnail = thumbnailCache.get(message.getUuid()); + Bitmap thumbnail = mXmppConnectionService.getBitmapCache().get( + message.getUuid()); if ((thumbnail == null) && (!cacheOnly)) { - File file = getJingleFile(message); - if (!file.exists()) { - file = getJingleFileLegacy(message); - } + File file = getFile(message); BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(file, size); Bitmap fullsize = BitmapFactory.decodeFile(file.getAbsolutePath(), @@ -258,32 +208,12 @@ public class FileBackend { throw new FileNotFoundException(); } thumbnail = resize(fullsize, size); - this.thumbnailCache.put(message.getUuid(), thumbnail); + this.mXmppConnectionService.getBitmapCache().put(message.getUuid(), + thumbnail); } return thumbnail; } - public void removeFiles(Conversation conversation) { - String prefix = context.getFilesDir().getAbsolutePath(); - String path = prefix + "/" + conversation.getAccount().getJid() + "/" - + conversation.getContactJid(); - File file = new File(path); - try { - this.deleteFile(file); - } catch (IOException e) { - Log.d(Config.LOGTAG, - "error deleting file: " + file.getAbsolutePath()); - } - } - - private void deleteFile(File f) throws IOException { - if (f.isDirectory()) { - for (File c : f.listFiles()) - deleteFile(c); - } - f.delete(); - } - public Uri getTakePhotoUri() { StringBuilder pathBuilder = new StringBuilder(); pathBuilder.append(Environment @@ -328,7 +258,7 @@ public class FileBackend { } public boolean isAvatarCached(Avatar avatar) { - File file = new File(getAvatarPath(context, avatar.getFilename())); + File file = new File(getAvatarPath(avatar.getFilename())); return file.exists(); } @@ -336,7 +266,7 @@ public class FileBackend { if (isAvatarCached(avatar)) { return true; } - String filename = getAvatarPath(context, avatar.getFilename()); + String filename = getAvatarPath(avatar.getFilename()); File file = new File(filename + ".tmp"); file.getParentFile().mkdirs(); try { @@ -368,15 +298,20 @@ public class FileBackend { } } - public static String getAvatarPath(Context context, String avatar) { - return context.getFilesDir().getAbsolutePath() + "/avatars/" + avatar; + public String getAvatarPath(String avatar) { + return mXmppConnectionService.getFilesDir().getAbsolutePath() + + "/avatars/" + avatar; + } + + public Uri getAvatarUri(String avatar) { + return Uri.parse("file:" + getAvatarPath(avatar)); } public Bitmap cropCenterSquare(Uri image, int size) { try { BitmapFactory.Options options = new BitmapFactory.Options(); options.inSampleSize = calcSampleSize(image, size); - InputStream is = context.getContentResolver() + InputStream is = mXmppConnectionService.getContentResolver() .openInputStream(image); Bitmap input = BitmapFactory.decodeStream(is, null, options); if (input == null) { @@ -393,7 +328,40 @@ public class FileBackend { } } - public static Bitmap cropCenterSquare(Bitmap input, int size) { + public Bitmap cropCenter(Uri image, int newHeight, int newWidth) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = calcSampleSize(image, + Math.max(newHeight, newWidth)); + InputStream is = mXmppConnectionService.getContentResolver() + .openInputStream(image); + Bitmap source = BitmapFactory.decodeStream(is, null, options); + + int sourceWidth = source.getWidth(); + int sourceHeight = source.getHeight(); + float xScale = (float) newWidth / sourceWidth; + float yScale = (float) newHeight / sourceHeight; + float scale = Math.max(xScale, yScale); + float scaledWidth = scale * sourceWidth; + float scaledHeight = scale * sourceHeight; + float left = (newWidth - scaledWidth) / 2; + float top = (newHeight - scaledHeight) / 2; + + RectF targetRect = new RectF(left, top, left + scaledWidth, top + + scaledHeight); + Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, + source.getConfig()); + Canvas canvas = new Canvas(dest); + canvas.drawBitmap(source, null, targetRect, null); + + return dest; + } catch (FileNotFoundException e) { + return null; + } + + } + + public Bitmap cropCenterSquare(Bitmap input, int size) { int w = input.getWidth(); int h = input.getHeight(); @@ -415,7 +383,7 @@ public class FileBackend { throws FileNotFoundException { BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(context.getContentResolver() + BitmapFactory.decodeStream(mXmppConnectionService.getContentResolver() .openInputStream(image), null, options); return calcSampleSize(options, size); } @@ -445,12 +413,8 @@ public class FileBackend { } public Uri getJingleFileUri(Message message) { - File file = getJingleFile(message); - if (file.exists()) { - return Uri.parse("file://" + file.getAbsolutePath()); - } else { - return ImageProvider.getProviderUri(message); - } + File file = getFile(message); + return Uri.parse("file://" + file.getAbsolutePath()); } public class ImageCopyException extends Exception { @@ -466,12 +430,18 @@ public class FileBackend { } } - public static Bitmap getAvatar(String avatar, int size, Context context) { - Bitmap bm = BitmapFactory.decodeFile(FileBackend.getAvatarPath(context, - avatar)); + public Bitmap getAvatar(String avatar, int size) { + if (avatar == null) { + return null; + } + Bitmap bm = cropCenter(getAvatarUri(avatar), size, size); if (bm == null) { return null; } - return cropCenterSquare(bm, UIHelper.getRealPx(size, context)); + return bm; + } + + public boolean isFileAvailable(Message message) { + return getFile(message).exists(); } } diff --git a/src/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/eu/siacs/conversations/services/AbstractConnectionManager.java new file mode 100644 index 00000000..676a09c9 --- /dev/null +++ b/src/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -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; + } + } +} diff --git a/src/eu/siacs/conversations/services/AvatarService.java b/src/eu/siacs/conversations/services/AvatarService.java new file mode 100644 index 00000000..bd27b555 --- /dev/null +++ b/src/eu/siacs/conversations/services/AvatarService.java @@ -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)]; + } + +} diff --git a/src/eu/siacs/conversations/services/ImageProvider.java b/src/eu/siacs/conversations/services/ImageProvider.java deleted file mode 100644 index ac78a454..00000000 --- a/src/eu/siacs/conversations/services/ImageProvider.java +++ /dev/null @@ -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"); - } -}
\ No newline at end of file diff --git a/src/eu/siacs/conversations/services/NotificationService.java b/src/eu/siacs/conversations/services/NotificationService.java index 41656707..7b2e16df 100644 --- a/src/eu/siacs/conversations/services/NotificationService.java +++ b/src/eu/siacs/conversations/services/NotificationService.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.services; +import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.regex.Matcher; @@ -11,70 +12,83 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.graphics.Bitmap; import android.net.Uri; import android.os.PowerManager; import android.os.SystemClock; import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationCompat.BigPictureStyle; +import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.app.TaskStackBuilder; import android.text.Html; +import android.util.DisplayMetrics; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ConversationActivity; public class NotificationService { private XmppConnectionService mXmppConnectionService; - private NotificationManager mNotificationManager; private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>(); - public int NOTIFICATION_ID = 0x2342; + public static int NOTIFICATION_ID = 0x2342; private Conversation mOpenConversation; private boolean mIsInForeground; - - private long mEndGracePeriod = 0L; + private long mLastNotification; public NotificationService(XmppConnectionService service) { this.mXmppConnectionService = service; - this.mNotificationManager = (NotificationManager) service - .getSystemService(Context.NOTIFICATION_SERVICE); } - public synchronized void push(Message message) { - + public void push(Message message) { PowerManager pm = (PowerManager) mXmppConnectionService .getSystemService(Context.POWER_SERVICE); boolean isScreenOn = pm.isScreenOn(); + if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) { return; } - String conversationUuid = message.getConversationUuid(); - if (notifications.containsKey(conversationUuid)) { - notifications.get(conversationUuid).add(message); - } else { - ArrayList<Message> mList = new ArrayList<Message>(); - mList.add(message); - notifications.put(conversationUuid, mList); + synchronized (notifications) { + String conversationUuid = message.getConversationUuid(); + if (notifications.containsKey(conversationUuid)) { + notifications.get(conversationUuid).add(message); + } else { + ArrayList<Message> mList = new ArrayList<Message>(); + mList.add(message); + notifications.put(conversationUuid, mList); + } + Account account = message.getConversation().getAccount(); + updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) + && !account.inGracePeriod() + && !this.inMiniGracePeriod(account)); } - updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) - && !inGracePeriod()); + } public void clear() { - notifications.clear(); - updateNotification(false); + synchronized (notifications) { + notifications.clear(); + updateNotification(false); + } } public void clear(Conversation conversation) { - notifications.remove(conversation.getUuid()); - updateNotification(false); + synchronized (notifications) { + notifications.remove(conversation.getUuid()); + updateNotification(false); + } } private void updateNotification(boolean notify) { + NotificationManager notificationManager = (NotificationManager) mXmppConnectionService + .getSystemService(Context.NOTIFICATION_SERVICE); SharedPreferences preferences = mXmppConnectionService.getPreferences(); String ringtone = preferences.getString("notification_ringtone", null); @@ -82,76 +96,16 @@ public class NotificationService { true); if (notifications.size() == 0) { - mNotificationManager.cancel(NOTIFICATION_ID); + notificationManager.cancel(NOTIFICATION_ID); } else { - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( - mXmppConnectionService); - mBuilder.setSmallIcon(R.drawable.ic_notification); + if (notify) { + this.markLastNotification(); + } + Builder mBuilder; if (notifications.size() == 1) { - ArrayList<Message> messages = notifications.values().iterator() - .next(); - if (messages.size() >= 1) { - Conversation conversation = messages.get(0) - .getConversation(); - mBuilder.setLargeIcon(conversation.getImage( - mXmppConnectionService, 64)); - mBuilder.setContentTitle(conversation.getName()); - StringBuilder text = new StringBuilder(); - for (int i = 0; i < messages.size(); ++i) { - text.append(messages.get(i).getReadableBody( - mXmppConnectionService)); - if (i != messages.size() - 1) { - text.append("\n"); - } - } - mBuilder.setStyle(new NotificationCompat.BigTextStyle() - .bigText(text.toString())); - mBuilder.setContentText(messages.get(0).getReadableBody( - mXmppConnectionService)); - if (notify) { - mBuilder.setTicker(messages.get(messages.size() - 1) - .getReadableBody(mXmppConnectionService)); - } - mBuilder.setContentIntent(createContentIntent(conversation - .getUuid())); - } else { - mNotificationManager.cancel(NOTIFICATION_ID); - return; - } + mBuilder = buildSingleConversations(notify); } else { - NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); - style.setBigContentTitle(notifications.size() - + " " - + mXmppConnectionService - .getString(R.string.unread_conversations)); - StringBuilder names = new StringBuilder(); - Conversation conversation = null; - for (ArrayList<Message> messages : notifications.values()) { - if (messages.size() > 0) { - conversation = messages.get(0).getConversation(); - String name = conversation.getName(); - style.addLine(Html.fromHtml("<b>" - + name - + "</b> " - + messages.get(0).getReadableBody( - mXmppConnectionService))); - names.append(name); - names.append(", "); - } - } - if (names.length() >= 2) { - names.delete(names.length() - 2, names.length()); - } - mBuilder.setContentTitle(notifications.size() - + " " - + mXmppConnectionService - .getString(R.string.unread_conversations)); - mBuilder.setContentText(names.toString()); - mBuilder.setStyle(style); - if (conversation != null) { - mBuilder.setContentIntent(createContentIntent(conversation - .getUuid())); - } + mBuilder = buildMultipleConversation(); } if (notify) { if (vibrate) { @@ -163,12 +117,147 @@ public class NotificationService { mBuilder.setSound(Uri.parse(ringtone)); } } + mBuilder.setSmallIcon(R.drawable.ic_notification); mBuilder.setDeleteIntent(createDeleteIntent()); - if (!inGracePeriod()) { - mBuilder.setLights(0xffffffff, 2000, 4000); - } + mBuilder.setLights(0xffffffff, 2000, 4000); Notification notification = mBuilder.build(); - mNotificationManager.notify(NOTIFICATION_ID, notification); + notificationManager.notify(NOTIFICATION_ID, notification); + } + } + + private Builder buildMultipleConversation() { + Builder mBuilder = new NotificationCompat.Builder( + mXmppConnectionService); + NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); + style.setBigContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + StringBuilder names = new StringBuilder(); + Conversation conversation = null; + for (ArrayList<Message> messages : notifications.values()) { + if (messages.size() > 0) { + conversation = messages.get(0).getConversation(); + String name = conversation.getName(); + style.addLine(Html.fromHtml("<b>" + name + "</b> " + + getReadableBody(messages.get(0)))); + names.append(name); + names.append(", "); + } + } + if (names.length() >= 2) { + names.delete(names.length() - 2, names.length()); + } + mBuilder.setContentTitle(notifications.size() + + " " + + mXmppConnectionService + .getString(R.string.unread_conversations)); + mBuilder.setContentText(names.toString()); + mBuilder.setStyle(style); + if (conversation != null) { + mBuilder.setContentIntent(createContentIntent(conversation + .getUuid())); + } + return mBuilder; + } + + private Builder buildSingleConversations(boolean notify) { + Builder mBuilder = new NotificationCompat.Builder( + mXmppConnectionService); + ArrayList<Message> messages = notifications.values().iterator().next(); + if (messages.size() >= 1) { + Conversation conversation = messages.get(0).getConversation(); + mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService() + .get(conversation, getPixel(64))); + mBuilder.setContentTitle(conversation.getName()); + Message message; + if ((message = getImage(messages)) != null) { + modifyForImage(mBuilder, message, messages, notify); + } else { + modifyForTextOnly(mBuilder, messages, notify); + } + mBuilder.setContentIntent(createContentIntent(conversation + .getUuid())); + } + return mBuilder; + + } + + private void modifyForImage(Builder builder, Message message, + ArrayList<Message> messages, boolean notify) { + try { + Bitmap bitmap = mXmppConnectionService.getFileBackend() + .getThumbnail(message, getPixel(288), false); + ArrayList<Message> tmp = new ArrayList<Message>(); + for (Message msg : messages) { + if (msg.getType() == Message.TYPE_TEXT + && msg.getDownloadable() == null) { + tmp.add(msg); + } + } + BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); + bigPictureStyle.bigPicture(bitmap); + if (tmp.size() > 0) { + bigPictureStyle.setSummaryText(getMergedBodies(tmp)); + builder.setContentText(getReadableBody(tmp.get(0))); + } else { + builder.setContentText(mXmppConnectionService.getString(R.string.image_file)); + } + builder.setStyle(bigPictureStyle); + } catch (FileNotFoundException e) { + modifyForTextOnly(builder, messages, notify); + } + } + + private void modifyForTextOnly(Builder builder, + ArrayList<Message> messages, boolean notify) { + builder.setStyle(new NotificationCompat.BigTextStyle() + .bigText(getMergedBodies(messages))); + builder.setContentText(getReadableBody(messages.get(0))); + if (notify) { + builder.setTicker(getReadableBody(messages.get(messages.size() - 1))); + } + } + + private Message getImage(ArrayList<Message> messages) { + for (Message message : messages) { + if (message.getType() == Message.TYPE_IMAGE + && message.getDownloadable() == null + && message.getEncryption() != Message.ENCRYPTION_PGP) { + return message; + } + } + return null; + } + + private String getMergedBodies(ArrayList<Message> messages) { + StringBuilder text = new StringBuilder(); + for (int i = 0; i < messages.size(); ++i) { + text.append(getReadableBody(messages.get(i))); + if (i != messages.size() - 1) { + text.append("\n"); + } + } + return text.toString(); + } + + private String getReadableBody(Message message) { + if (message.getDownloadable() != null + && (message.getDownloadable().getStatus() == Downloadable.STATUS_OFFER || message + .getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) { + return mXmppConnectionService.getText( + R.string.image_offered_for_download).toString(); + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + return mXmppConnectionService.getText( + R.string.encrypted_message_received).toString(); + } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { + return mXmppConnectionService.getText(R.string.decryption_failed) + .toString(); + } else if (message.getType() == Message.TYPE_IMAGE) { + return mXmppConnectionService.getText(R.string.image_file) + .toString(); + } else { + return message.getBody().trim(); } } @@ -226,16 +315,19 @@ public class NotificationService { this.mIsInForeground = foreground; } - public void activateGracePeriod() { - this.mEndGracePeriod = SystemClock.elapsedRealtime() - + (Config.CARBON_GRACE_PERIOD * 1000); + private int getPixel(int dp) { + DisplayMetrics metrics = mXmppConnectionService.getResources() + .getDisplayMetrics(); + return ((int) (dp * metrics.density)); } - public void deactivateGracePeriod() { - this.mEndGracePeriod = 0L; + private void markLastNotification() { + this.mLastNotification = SystemClock.elapsedRealtime(); } - private boolean inGracePeriod() { - return SystemClock.elapsedRealtime() < this.mEndGracePeriod; + private boolean inMiniGracePeriod(Account account) { + int miniGrace = account.getStatus() == Account.STATUS_ONLINE ? Config.MINI_GRACE_PERIOD + : Config.MINI_GRACE_PERIOD * 2; + return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); } } diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java index e6297f4f..be73e07f 100644 --- a/src/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/eu/siacs/conversations/services/XmppConnectionService.java @@ -27,6 +27,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; @@ -34,6 +35,7 @@ import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.generator.MessageGenerator; import eu.siacs.conversations.generator.PresenceGenerator; +import eu.siacs.conversations.http.HttpConnectionManager; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.parser.MessageParser; import eu.siacs.conversations.parser.PresenceParser; @@ -74,6 +76,7 @@ import android.net.NetworkInfo; import android.net.Uri; import android.os.Binder; import android.os.Bundle; +import android.os.FileObserver; import android.os.IBinder; import android.os.PowerManager; import android.os.PowerManager.WakeLock; @@ -81,11 +84,12 @@ import android.os.SystemClock; import android.preference.PreferenceManager; import android.provider.ContactsContract; import android.util.Log; +import android.util.LruCache; public class XmppConnectionService extends Service { public DatabaseBackend databaseBackend; - private FileBackend fileBackend; + private FileBackend fileBackend = new FileBackend(this); public long startDate; @@ -94,7 +98,8 @@ public class XmppConnectionService extends Service { private MemorizingTrustManager mMemorizingTrustManager; - private NotificationService mNotificationService; + private NotificationService mNotificationService = new NotificationService( + this); private MessageParser mMessageParser = new MessageParser(this); private PresenceParser mPresenceParser = new PresenceParser(this); @@ -106,20 +111,27 @@ public class XmppConnectionService extends Service { private CopyOnWriteArrayList<Conversation> conversations = null; private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( this); + private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( + this); + private AvatarService mAvatarService = new AvatarService(this); private OnConversationUpdate mOnConversationUpdate = null; - private int convChangedListenerCount = 0; + private Integer convChangedListenerCount = 0; private OnAccountUpdate mOnAccountUpdate = null; - private int accountChangedListenerCount = 0; + private Integer accountChangedListenerCount = 0; private OnRosterUpdate mOnRosterUpdate = null; - private int rosterChangedListenerCount = 0; + private Integer rosterChangedListenerCount = 0; public OnContactStatusChanged onContactStatusChanged = new OnContactStatusChanged() { @Override public void onContactStatusChanged(Contact contact, boolean online) { Conversation conversation = find(getConversations(), contact); if (conversation != null) { - conversation.endOtrIfNeeded(); + if (online && contact.getPresences().size() > 1) { + conversation.endOtrIfNeeded(); + } else { + conversation.resetOtrSession(); + } if (online && (contact.getPresences().size() == 1)) { sendUnsendMessages(conversation); } @@ -140,6 +152,17 @@ public class XmppConnectionService extends Service { } }; + private FileObserver fileObserver = new FileObserver( + FileBackend.getConversationsDirectory()) { + + @Override + public void onEvent(int event, String path) { + if (event == FileObserver.DELETE) { + markFileDeleted(path.split("\\.")[0]); + } + } + }; + private final IBinder mBinder = new XmppConnectionBinder(); private OnStatusChanged statusListener = new OnStatusChanged() { @@ -252,6 +275,7 @@ public class XmppConnectionService extends Service { } } }; + private LruCache<String, Bitmap> mBitmapCache; public PgpEngine getPgpEngine() { if (pgpServiceConnection.isBound()) { @@ -271,6 +295,10 @@ public class XmppConnectionService extends Service { return this.fileBackend; } + public AvatarService getAvatarService() { + return this.mAvatarService; + } + public Message attachImageToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { final Message message; @@ -331,15 +359,10 @@ public class XmppConnectionService extends Service { } } this.wakeLock.acquire(); - ConnectivityManager cm = (ConnectivityManager) getApplicationContext() - .getSystemService(Context.CONNECTIVITY_SERVICE); - NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); - boolean isConnected = activeNetwork != null - && activeNetwork.isConnected(); for (Account account : accounts) { if (!account.isOptionSet(Account.OPTION_DISABLED)) { - if (!isConnected) { + if (!hasInternetConnection()) { account.setStatus(Account.STATUS_NO_INTERNET); if (statusListener != null) { statusListener.onStatusChanged(account); @@ -398,6 +421,13 @@ public class XmppConnectionService extends Service { return START_STICKY; } + public boolean hasInternetConnection() { + ConnectivityManager cm = (ConnectivityManager) getApplicationContext() + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo activeNetwork = cm.getActiveNetworkInfo(); + return activeNetwork != null && activeNetwork.isConnected(); + } + @SuppressLint("TrulyRandom") @Override public void onCreate() { @@ -406,10 +436,18 @@ public class XmppConnectionService extends Service { this.mRandom = new SecureRandom(); this.mMemorizingTrustManager = new MemorizingTrustManager( getApplicationContext()); - this.mNotificationService = new NotificationService(this); + + int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + int cacheSize = maxMemory / 8; + this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) { + @Override + protected int sizeOf(String key, Bitmap bitmap) { + return bitmap.getByteCount() / 1024; + } + }; + this.databaseBackend = DatabaseBackend .getInstance(getApplicationContext()); - this.fileBackend = new FileBackend(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); for (Account account : this.accounts) { @@ -420,6 +458,7 @@ public class XmppConnectionService extends Service { getContentResolver().registerContentObserver( ContactsContract.Contacts.CONTENT_URI, true, contactObserver); + this.fileObserver.startWatching(); this.pgpServiceConnection = new OpenPgpServiceConnection( getApplicationContext(), "org.sufficientlysecure.keychain"); this.pgpServiceConnection.bindToService(); @@ -511,8 +550,9 @@ public class XmppConnectionService extends Service { return connection; } - synchronized public void sendMessage(Message message) { + public void sendMessage(Message message) { Account account = message.getConversation().getAccount(); + account.deactivateGracePeriod(); Conversation conv = message.getConversation(); MessagePacket packet = null; boolean saveInDb = true; @@ -531,13 +571,14 @@ public class XmppConnectionService extends Service { && conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) { mJingleConnectionManager .createNewConnection(message); - } else if (message.getPresence() == null) { - message.setStatus(Message.STATUS_WAITING); } } else { mJingleConnectionManager.createNewConnection(message); } } else { + if (message.getEncryption() == Message.ENCRYPTION_OTR) { + conv.startOtrIfNeeded(); + } message.setStatus(Message.STATUS_WAITING); } } else { @@ -554,6 +595,7 @@ public class XmppConnectionService extends Service { send = true; } else if (message.getPresence() == null) { + conv.startOtrIfNeeded(); message.setStatus(Message.STATUS_WAITING); } } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { @@ -596,7 +638,7 @@ public class XmppConnectionService extends Service { } } - conv.getMessages().add(message); + conv.add(message); if (saveInDb) { if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) { @@ -776,6 +818,7 @@ public class XmppConnectionService extends Service { .getString("photouri")); contact.setSystemName(phoneContact .getString("displayname")); + getAvatarService().clear(contact); } } } @@ -794,12 +837,39 @@ public class XmppConnectionService extends Service { Account account = accountLookupTable.get(conv.getAccountUuid()); conv.setAccount(account); conv.setMessages(databaseBackend.getMessages(conv, 50)); + checkDeletedFiles(conv); } } - return this.conversations; } + private void checkDeletedFiles(Conversation conversation) { + for (Message message : conversation.getMessages()) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DeletedDownloadable()); + } + } + } + } + + private void markFileDeleted(String uuid) { + for (Conversation conversation : getConversations()) { + for (Message message : conversation.getMessages()) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DeletedDownloadable()); + updateConversationUi(); + } + return; + } + } + } + } + public void populateWithOrderedConversations(List<Conversation> list) { populateWithOrderedConversations(list, true); } @@ -838,7 +908,7 @@ public class XmppConnectionService extends Service { for (Message message : messages) { message.setConversation(conversation); } - conversation.getMessages().addAll(0, messages); + conversation.addAll(0, messages); return messages.size(); } @@ -858,9 +928,9 @@ public class XmppConnectionService extends Service { public Conversation find(List<Conversation> haystack, Account account, String jid) { for (Conversation conversation : haystack) { - if ((account == null || conversation.getAccount().equals(account)) + if ((account == null || conversation.getAccount() == account) && (conversation.getContactJid().split("/", 2)[0] - .equals(jid))) { + .equalsIgnoreCase(jid))) { return conversation; } } @@ -927,7 +997,6 @@ public class XmppConnectionService extends Service { public void clearConversationHistory(Conversation conversation) { this.databaseBackend.deleteMessagesInConversation(conversation); - this.fileBackend.removeFiles(conversation); conversation.getMessages().clear(); updateConversationUi(); } @@ -973,60 +1042,85 @@ public class XmppConnectionService extends Service { public void setOnConversationListChangedListener( OnConversationUpdate listener) { - this.mNotificationService.deactivateGracePeriod(); - if (checkListeners()) { - switchToForeground(); + if (!isScreenOn()) { + Log.d(Config.LOGTAG, + "ignoring setOnConversationListChangedListener"); + return; + } + synchronized (this.convChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnConversationUpdate = listener; + this.mNotificationService.setIsInForeground(true); + this.convChangedListenerCount++; } - this.mOnConversationUpdate = listener; - this.mNotificationService.setIsInForeground(true); - this.convChangedListenerCount++; } public void removeOnConversationListChangedListener() { - this.convChangedListenerCount--; - if (this.convChangedListenerCount == 0) { - this.mOnConversationUpdate = null; - this.mNotificationService.setIsInForeground(false); - if (checkListeners()) { - switchToBackground(); + synchronized (this.convChangedListenerCount) { + this.convChangedListenerCount--; + if (this.convChangedListenerCount <= 0) { + this.convChangedListenerCount = 0; + this.mOnConversationUpdate = null; + this.mNotificationService.setIsInForeground(false); + if (checkListeners()) { + switchToBackground(); + } } } } public void setOnAccountListChangedListener(OnAccountUpdate listener) { - this.mNotificationService.deactivateGracePeriod(); - if (checkListeners()) { - switchToForeground(); + if (!isScreenOn()) { + Log.d(Config.LOGTAG, "ignoring setOnAccountListChangedListener"); + return; + } + synchronized (this.accountChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnAccountUpdate = listener; + this.accountChangedListenerCount++; } - this.mOnAccountUpdate = listener; - this.accountChangedListenerCount++; } public void removeOnAccountListChangedListener() { - this.accountChangedListenerCount--; - if (this.accountChangedListenerCount == 0) { - this.mOnAccountUpdate = null; - if (checkListeners()) { - switchToBackground(); + synchronized (this.accountChangedListenerCount) { + this.accountChangedListenerCount--; + if (this.accountChangedListenerCount <= 0) { + this.mOnAccountUpdate = null; + this.accountChangedListenerCount = 0; + if (checkListeners()) { + switchToBackground(); + } } } } public void setOnRosterUpdateListener(OnRosterUpdate listener) { - this.mNotificationService.deactivateGracePeriod(); - if (checkListeners()) { - switchToForeground(); + if (!isScreenOn()) { + Log.d(Config.LOGTAG, "ignoring setOnRosterUpdateListener"); + return; + } + synchronized (this.rosterChangedListenerCount) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnRosterUpdate = listener; + this.rosterChangedListenerCount++; } - this.mOnRosterUpdate = listener; - this.rosterChangedListenerCount++; } public void removeOnRosterUpdateListener() { - this.rosterChangedListenerCount--; - if (this.rosterChangedListenerCount == 0) { - this.mOnRosterUpdate = null; - if (checkListeners()) { - switchToBackground(); + synchronized (this.rosterChangedListenerCount) { + this.rosterChangedListenerCount--; + if (this.rosterChangedListenerCount <= 0) { + this.rosterChangedListenerCount = 0; + this.mOnRosterUpdate = null; + if (checkListeners()) { + switchToBackground(); + } } } } @@ -1042,11 +1136,10 @@ public class XmppConnectionService extends Service { XmppConnection connection = account.getXmppConnection(); if (connection != null && connection.getFeatures().csi()) { connection.sendActive(); - Log.d(Config.LOGTAG, account.getJid() - + " sending csi//active"); } } } + Log.d(Config.LOGTAG, "app switched into foreground"); } private void switchToBackground() { @@ -1055,11 +1148,17 @@ public class XmppConnectionService extends Service { XmppConnection connection = account.getXmppConnection(); if (connection != null && connection.getFeatures().csi()) { connection.sendInactive(); - Log.d(Config.LOGTAG, account.getJid() - + " sending csi//inactive"); } } } + this.mNotificationService.setIsInForeground(false); + Log.d(Config.LOGTAG, "app switched into background"); + } + + private boolean isScreenOn() { + PowerManager pm = (PowerManager) this + .getSystemService(Context.POWER_SERVICE); + return pm.isScreenOn(); } public void connectMultiModeConversations(Account account) { @@ -1197,7 +1296,7 @@ public class XmppConnectionService extends Service { conversation.getMucOptions().setOffline(); conversation.deregisterWithBookmark(); Log.d(Config.LOGTAG, conversation.getAccount().getJid() - + " leaving muc " + conversation.getContactJid()); + + ": leaving muc " + conversation.getContactJid()); } else { account.pendingConferenceLeaves.add(conversation); } @@ -1214,7 +1313,11 @@ public class XmppConnectionService extends Service { if (conversation.getMode() == Conversation.MODE_MULTI) { leaveMuc(conversation); } else { - conversation.endOtrIfNeeded(); + if (conversation.endOtrIfNeeded()) { + Log.d(Config.LOGTAG, account.getJid() + + ": ended otr session with " + + conversation.getContactJid()); + } } } } @@ -1230,6 +1333,7 @@ public class XmppConnectionService extends Service { public void updateMessage(Message message) { databaseBackend.updateMessage(message); + updateConversationUi(); } protected void syncDirtyContacts(Account account) { @@ -1410,10 +1514,16 @@ public class XmppConnectionService extends Service { if (account.setAvatar(avatar.getFilename())) { databaseBackend.updateAccount(account); } + getAvatarService().clear(account); + updateConversationUi(); + updateAccountUi(); } else { Contact contact = account.getRoster() .getContact(avatar.owner); contact.setAvatar(avatar.getFilename()); + getAvatarService().clear(contact); + updateConversationUi(); + updateRosterUi(); } if (callback != null) { callback.success(avatar); @@ -1463,6 +1573,7 @@ public class XmppConnectionService extends Service { if (account.setAvatar(avatar.getFilename())) { databaseBackend.updateAccount(account); } + getAvatarService().clear(account); callback.success(avatar); } else { fetchAvatar(account, avatar, callback); @@ -1675,6 +1786,10 @@ public class XmppConnectionService extends Service { return this.pm; } + public LruCache<String, Bitmap> getBitmapCache() { + return this.mBitmapCache; + } + public void replyWithNotAcceptable(Account account, MessagePacket packet) { if (account.getStatus() == Account.STATUS_ONLINE) { MessagePacket error = this.mMessageGenerator @@ -1779,8 +1894,7 @@ public class XmppConnectionService extends Service { ArrayList<Contact> contacts = new ArrayList<Contact>(); for (Account account : getAccounts()) { if (!account.isOptionSet(Account.OPTION_DISABLED)) { - Contact contact = account.getRoster() - .getContactAsShownInRoster(jid); + Contact contact = account.getRoster().getContactFromRoster(jid); if (contact != null) { contacts.add(contact); } @@ -1792,4 +1906,44 @@ public class XmppConnectionService extends Service { public NotificationService getNotificationService() { return this.mNotificationService; } + + public HttpConnectionManager getHttpConnectionManager() { + return this.mHttpConnectionManager; + } + + private class DeletedDownloadable implements Downloadable { + + @Override + public boolean start() { + return false; + } + + @Override + public int getStatus() { + return Downloadable.STATUS_DELETED; + } + + @Override + public long getFileSize() { + return 0; + } + + } + + public void resendFailedMessages(Message message) { + List<Message> messages = new ArrayList<Message>(); + Message current = message; + while(current.getStatus() == Message.STATUS_SEND_FAILED) { + messages.add(current); + if (current.mergable(current.next())) { + current = current.next(); + } else { + break; + } + } + for(Message msg: messages) { + markMessage(msg, Message.STATUS_WAITING); + this.resendMessage(msg); + } + } } diff --git a/src/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/eu/siacs/conversations/ui/ChooseContactActivity.java index 62a2cbe1..f14da352 100644 --- a/src/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -113,7 +113,7 @@ public class ChooseContactActivity extends XmppActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.choose_contact, menu); - MenuItem menuSearchView = (MenuItem) menu.findItem(R.id.action_search); + MenuItem menuSearchView = menu.findItem(R.id.action_search); View mSearchView = menuSearchView.getActionView(); mSearchEditText = (EditText) mSearchView .findViewById(R.id.search_field); diff --git a/src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 04059d52..52687c81 100644 --- a/src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -7,14 +7,12 @@ import org.openintents.openpgp.util.OpenPgpUtils; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdate; -import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import android.app.PendingIntent; import android.content.Context; @@ -41,6 +39,7 @@ public class ConferenceDetailsActivity extends XmppActivity { private ImageButton mEditNickButton; private TextView mRoleAffiliaton; private TextView mFullJid; + private TextView mAccountJid; private LinearLayout membersView; private LinearLayout mMoreDetails; private Button mInviteButton; @@ -78,6 +77,7 @@ public class ConferenceDetailsActivity extends XmppActivity { mEditNickButton = (ImageButton) findViewById(R.id.edit_nick_button); mFullJid = (TextView) findViewById(R.id.muc_jabberid); membersView = (LinearLayout) findViewById(R.id.muc_members); + mAccountJid = (TextView) findViewById(R.id.details_account); mMoreDetails = (LinearLayout) findViewById(R.id.muc_more_details); mMoreDetails.setVisibility(View.GONE); mInviteButton = (Button) findViewById(R.id.invite); @@ -169,37 +169,38 @@ public class ConferenceDetailsActivity extends XmppActivity { } protected void registerListener() { - if (xmppConnectionServiceBound) { - xmppConnectionService - .setOnConversationListChangedListener(this.onConvChanged); - xmppConnectionService.setOnRenameListener(new OnRenameListener() { + xmppConnectionService + .setOnConversationListChangedListener(this.onConvChanged); + xmppConnectionService.setOnRenameListener(new OnRenameListener() { - @Override - public void onRename(final boolean success) { - runOnUiThread(new Runnable() { + @Override + public void onRename(final boolean success) { + runOnUiThread(new Runnable() { - @Override - public void run() { - populateView(); - if (success) { - Toast.makeText( - ConferenceDetailsActivity.this, - getString(R.string.your_nick_has_been_changed), - Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(ConferenceDetailsActivity.this, - getString(R.string.nick_in_use), - Toast.LENGTH_SHORT).show(); - } + @Override + public void run() { + populateView(); + if (success) { + Toast.makeText( + ConferenceDetailsActivity.this, + getString(R.string.your_nick_has_been_changed), + Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(ConferenceDetailsActivity.this, + getString(R.string.nick_in_use), + Toast.LENGTH_SHORT).show(); } - }); - } - }); - } + } + }); + } + }); } private void populateView() { - mYourPhoto.setImageBitmap(conversation.getAccount().getImage(this, 48)); + mAccountJid.setText(getString(R.string.using_account, conversation + .getAccount().getJid())); + mYourPhoto.setImageBitmap(avatarService().get( + conversation.getAccount(), getPixel(48))); setTitle(conversation.getName()); mFullJid.setText(conversation.getContactJid().split("/", 2)[0]); mYourNick.setText(conversation.getMucOptions().getActualNick()); @@ -225,9 +226,8 @@ public class ConferenceDetailsActivity extends XmppActivity { this.users.addAll(conversation.getMucOptions().getUsers()); LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); membersView.removeAllViews(); - Account account = conversation.getAccount(); for (final User user : conversation.getMucOptions().getUsers()) { - View view = (View) inflater.inflate(R.layout.contact, membersView, + View view = inflater.inflate(R.layout.contact, membersView, false); TextView name = (TextView) view .findViewById(R.id.contact_display_name); @@ -245,22 +245,14 @@ public class ConferenceDetailsActivity extends XmppActivity { key.setText(OpenPgpUtils.convertKeyIdToHex(user.getPgpKeyId())); } Bitmap bm; - if (user.getJid() != null) { - Contact contact = account.getRoster().getContact(user.getJid()); - if (contact.showInRoster()) { - bm = contact.getImage(48, this); - name.setText(contact.getDisplayName()); - role.setText(user.getName() + " \u2022 " - + getReadableRole(user.getRole())); - } else { - bm = UIHelper.getContactPicture(user.getName(), 48, this, - false); - name.setText(user.getName()); - role.setText(getReadableRole(user.getRole())); - } + Contact contact = user.getContact(); + if (contact != null) { + bm = avatarService().get(contact, getPixel(48)); + name.setText(contact.getDisplayName()); + role.setText(user.getName() + " \u2022 " + + getReadableRole(user.getRole())); } else { - bm = UIHelper - .getContactPicture(user.getName(), 48, this, false); + bm = avatarService().get(user.getName(), getPixel(48)); name.setText(user.getName()); role.setText(getReadableRole(user.getRole())); } diff --git a/src/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/eu/siacs/conversations/ui/ContactDetailsActivity.java index 9926e126..4c52c609 100644 --- a/src/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -56,7 +56,8 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(DialogInterface dialog, int which) { - ContactDetailsActivity.this.xmppConnectionService.deleteContactOnServer(contact); + ContactDetailsActivity.this.xmppConnectionService + .deleteContactOnServer(contact); ContactDetailsActivity.this.finish(); } }; @@ -78,7 +79,8 @@ public class ContactDetailsActivity extends XmppActivity { @Override public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder(ContactDetailsActivity.this); + AlertDialog.Builder builder = new AlertDialog.Builder( + ContactDetailsActivity.this); builder.setTitle(getString(R.string.action_add_phone_book)); builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid())); @@ -309,22 +311,21 @@ public class ContactDetailsActivity extends XmppActivity { } else { contactJidTv.setText(contact.getJid()); } - accountJidTv.setText(contact.getAccount().getJid()); - - UIHelper.prepareContactBadge(this, badge, contact, - getApplicationContext()); - + accountJidTv.setText(getString(R.string.using_account, contact + .getAccount().getJid())); + prepareContactBadge(badge, contact); if (contact.getSystemAccount() == null) { badge.setOnClickListener(onBadgeClick); } keys.removeAllViews(); + boolean hasKeys = false; LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); for (Iterator<String> iterator = contact.getOtrFingerprints() .iterator(); iterator.hasNext();) { + hasKeys = true; final String otrFingerprint = iterator.next(); - View view = (View) inflater.inflate(R.layout.contact_key, keys, - false); + View view = inflater.inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); ImageButton remove = (ImageButton) view @@ -342,8 +343,8 @@ public class ContactDetailsActivity extends XmppActivity { }); } if (contact.getPgpKeyId() != 0) { - View view = (View) inflater.inflate(R.layout.contact_key, keys, - false); + hasKeys = true; + View view = inflater.inflate(R.layout.contact_key, keys, false); TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); keyType.setText("PGP Key ID"); @@ -370,6 +371,20 @@ public class ContactDetailsActivity extends XmppActivity { }); keys.addView(view); } + if (hasKeys) { + keys.setVisibility(View.VISIBLE); + } else { + keys.setVisibility(View.GONE); + } + } + + private void prepareContactBadge(QuickContactBadge badge, Contact contact) { + if (contact.getSystemAccount() != null) { + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1])); + } + badge.setImageBitmap(avatarService().get(contact, getPixel(72))); } protected void confirmToDeleteFingerprint(final String fingerprint) { diff --git a/src/eu/siacs/conversations/ui/ConversationActivity.java b/src/eu/siacs/conversations/ui/ConversationActivity.java index ad1cd283..1d7364d6 100644 --- a/src/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/eu/siacs/conversations/ui/ConversationActivity.java @@ -191,6 +191,7 @@ public class ConversationActivity extends XmppActivity implements xmppConnectionService.getNotificationService() .setOpenConversation(null); } + closeContextMenu(); } @Override @@ -222,7 +223,8 @@ public class ConversationActivity extends XmppActivity implements ab.setDisplayHomeAsUpEnabled(true); ab.setHomeButtonEnabled(true); if (getSelectedConversation().getMode() == Conversation.MODE_SINGLE - || ConversationActivity.this.useSubjectToIdentifyConference()) { + || ConversationActivity.this + .useSubjectToIdentifyConference()) { ab.setTitle(getSelectedConversation().getName()); } else { ab.setTitle(getSelectedConversation().getContactJid() @@ -239,19 +241,16 @@ public class ConversationActivity extends XmppActivity implements @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.conversations, menu); - MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security); - MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive); - MenuItem menuMucDetails = (MenuItem) menu - .findItem(R.id.action_muc_details); - MenuItem menuContactDetails = (MenuItem) menu + MenuItem menuSecure = menu.findItem(R.id.action_security); + MenuItem menuArchive = menu.findItem(R.id.action_archive); + MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details); + MenuItem menuContactDetails = menu .findItem(R.id.action_contact_details); - MenuItem menuAttach = (MenuItem) menu.findItem(R.id.action_attach_file); - MenuItem menuClearHistory = (MenuItem) menu - .findItem(R.id.action_clear_history); - MenuItem menuAdd = (MenuItem) menu.findItem(R.id.action_add); - MenuItem menuInviteContact = (MenuItem) menu - .findItem(R.id.action_invite); - MenuItem menuMute = (MenuItem) menu.findItem(R.id.action_mute); + MenuItem menuAttach = menu.findItem(R.id.action_attach_file); + MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history); + MenuItem menuAdd = menu.findItem(R.id.action_add); + MenuItem menuInviteContact = menu.findItem(R.id.action_invite); + MenuItem menuMute = menu.findItem(R.id.action_mute); if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) { @@ -604,8 +603,11 @@ public class ConversationActivity extends XmppActivity implements .beginTransaction(); transaction.replace(R.id.selected_conversation, selectedFragment, "conversation"); - - transaction.commitAllowingStateLoss(); + try { + transaction.commitAllowingStateLoss(); + } catch (IllegalStateException e) { + return selectedFragment; + } } return selectedFragment; } @@ -624,23 +626,10 @@ public class ConversationActivity extends XmppActivity implements @Override protected void onNewIntent(Intent intent) { if (xmppConnectionServiceBound) { - if ((Intent.ACTION_VIEW.equals(intent.getAction()) && (VIEW_CONVERSATION - .equals(intent.getType())))) { - String convToView = (String) intent.getExtras().get( - CONVERSATION); - updateConversationList(); - for (int i = 0; i < conversationList.size(); ++i) { - if (conversationList.get(i).getUuid().equals(convToView)) { - setSelectedConversation(conversationList.get(i)); - break; - } - } - paneShouldBeOpen = false; - String text = intent.getExtras().getString(TEXT, null); - swapConversationFragment().setText(text); + if (intent != null && VIEW_CONVERSATION.equals(intent.getType())) { + handleViewConversationIntent(intent); } } else { - handledViewIntent = false; setIntent(intent); } } @@ -690,6 +679,10 @@ public class ConversationActivity extends XmppActivity implements } else if (conversationList.size() <= 0) { startActivity(new Intent(this, StartConversationActivity.class)); finish(); + } else if (getIntent() != null + && VIEW_CONVERSATION.equals(getIntent().getType())) { + handleViewConversationIntent(getIntent()); + setIntent(null); } else if (mOpenConverstaion != null) { selectConversationByUuid(mOpenConverstaion); paneShouldBeOpen = mPanelOpen; @@ -698,14 +691,6 @@ public class ConversationActivity extends XmppActivity implements } swapConversationFragment(); mOpenConverstaion = null; - } else if (getIntent() != null - && VIEW_CONVERSATION.equals(getIntent().getType())) { - String uuid = (String) getIntent().getExtras().get(CONVERSATION); - String text = getIntent().getExtras().getString(TEXT, null); - selectConversationByUuid(uuid); - paneShouldBeOpen = false; - swapConversationFragment().setText(text); - setIntent(null); } else { showConversationsOverview(); ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager() @@ -727,6 +712,14 @@ public class ConversationActivity extends XmppActivity implements ExceptionHelper.checkForCrash(this, this.xmppConnectionService); } + private void handleViewConversationIntent(Intent intent) { + String uuid = (String) intent.getExtras().get(CONVERSATION); + String text = intent.getExtras().getString(TEXT, null); + selectConversationByUuid(uuid); + paneShouldBeOpen = false; + swapConversationFragment().setText(text); + } + private void selectConversationByUuid(String uuid) { for (int i = 0; i < conversationList.size(); ++i) { if (conversationList.get(i).getUuid().equals(uuid)) { @@ -736,11 +729,9 @@ public class ConversationActivity extends XmppActivity implements } public void registerListener() { - if (xmppConnectionServiceBound) { - xmppConnectionService.setOnConversationListChangedListener(this); - xmppConnectionService.setOnAccountListChangedListener(this); - xmppConnectionService.setOnRosterUpdateListener(this); - } + xmppConnectionService.setOnConversationListChangedListener(this); + xmppConnectionService.setOnAccountListChangedListener(this); + xmppConnectionService.setOnRosterUpdateListener(this); } @Override @@ -753,6 +744,7 @@ public class ConversationActivity extends XmppActivity implements .findFragmentByTag("conversation"); if (selectedFragment != null) { selectedFragment.hideSnackbar(); + selectedFragment.updateMessages(); } } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { pendingImageUri = data.getData(); @@ -786,6 +778,10 @@ public class ConversationActivity extends XmppActivity implements attachAudioToConversation(getSelectedConversation(), data.getData()); } + } else { + if (requestCode == REQUEST_IMAGE_CAPTURE) { + pendingImageUri = null; + } } } diff --git a/src/eu/siacs/conversations/ui/ConversationFragment.java b/src/eu/siacs/conversations/ui/ConversationFragment.java index cdaa7152..20eeeb30 100644 --- a/src/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/eu/siacs/conversations/ui/ConversationFragment.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.ui; import java.util.ArrayList; import java.util.List; import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; import net.java.otr4j.session.SessionStatus; import eu.siacs.conversations.R; @@ -32,9 +33,12 @@ import android.content.IntentSender.SendIntentException; import android.os.Bundle; import android.text.Editable; import android.text.Selection; +import android.view.ContextMenu; +import android.view.ContextMenu.ContextMenuInfo; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; @@ -44,6 +48,8 @@ import android.widget.AbsListView.OnScrollListener; import android.widget.TextView.OnEditorActionListener; import android.widget.AbsListView; +import android.widget.AdapterView; +import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.ListView; import android.widget.ImageButton; import android.widget.RelativeLayout; @@ -72,6 +78,9 @@ public class ConversationFragment extends Fragment { private IntentSender askForPassphraseIntent = null; + private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<Message>(); + private boolean mDecryptJobRunning = false; + private OnEditorActionListener mEditorActionListener = new OnEditorActionListener() { @Override @@ -189,6 +198,7 @@ public class ConversationFragment extends Fragment { }; private ConversationActivity activity; + private Message selectedMessage; private void sendMessage() { if (this.conversation == null) { @@ -322,9 +332,114 @@ public class ConversationFragment extends Fragment { }); messagesView.setAdapter(messageListAdapter); + registerForContextMenu(messagesView); + return view; } + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + this.selectedMessage = this.messageList.get(acmi.position); + populateContextMenu(menu); + } + + private void populateContextMenu(ContextMenu menu) { + if (this.selectedMessage.getType() != Message.TYPE_STATUS) { + activity.getMenuInflater().inflate(R.menu.message_context, menu); + menu.setHeaderTitle(R.string.message_options); + MenuItem copyText = menu.findItem(R.id.copy_text); + MenuItem shareImage = menu.findItem(R.id.share_image); + MenuItem sendAgain = menu.findItem(R.id.send_again); + MenuItem copyUrl = menu.findItem(R.id.copy_url); + MenuItem downloadImage = menu.findItem(R.id.download_image); + if (this.selectedMessage.getType() != Message.TYPE_TEXT + || this.selectedMessage.getDownloadable() != null) { + copyText.setVisible(false); + } + if (this.selectedMessage.getType() != Message.TYPE_IMAGE + || this.selectedMessage.getDownloadable() != null) { + shareImage.setVisible(false); + } + if (this.selectedMessage.getStatus() != Message.STATUS_SEND_FAILED) { + sendAgain.setVisible(false); + } + if ((this.selectedMessage.getType() != Message.TYPE_IMAGE && this.selectedMessage + .getDownloadable() == null) + || this.selectedMessage.getImageParams().url == null) { + copyUrl.setVisible(false); + } + + if (this.selectedMessage.getType() != Message.TYPE_TEXT + || this.selectedMessage.getDownloadable() != null + || !this.selectedMessage.bodyContainsDownloadable()) { + downloadImage.setVisible(false); + } + } + } + + @Override + public boolean onContextItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.share_image: + shareImage(selectedMessage); + return true; + case R.id.copy_text: + copyText(selectedMessage); + return true; + case R.id.send_again: + resendMessage(selectedMessage); + return true; + case R.id.copy_url: + copyUrl(selectedMessage); + return true; + case R.id.download_image: + downloadImage(selectedMessage); + return true; + default: + return super.onContextItemSelected(item); + } + } + + private void shareImage(Message message) { + Intent shareIntent = new Intent(); + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, + activity.xmppConnectionService.getFileBackend() + .getJingleFileUri(message)); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + shareIntent.setType("image/webp"); + activity.startActivity(Intent.createChooser(shareIntent, + getText(R.string.share_with))); + } + + private void copyText(Message message) { + if (activity.copyTextToClipboard(message.getMergedBody(), + R.string.message_text)) { + Toast.makeText(activity, R.string.message_copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + } + } + + private void resendMessage(Message message) { + activity.xmppConnectionService.resendFailedMessages(message); + } + + private void copyUrl(Message message) { + if (activity.copyTextToClipboard( + message.getImageParams().url.toString(), R.string.image_url)) { + Toast.makeText(activity, R.string.url_copied_to_clipboard, + Toast.LENGTH_SHORT).show(); + } + } + + private void downloadImage(Message message) { + activity.xmppConnectionService.getHttpConnectionManager() + .createNewConnection(message); + } + protected void privateMessageWith(String counterpart) { this.mEditMessage.setText(""); this.conversation.setNextPresence(counterpart); @@ -356,6 +471,7 @@ public class ConversationFragment extends Fragment { @Override public void onStop() { + mDecryptJobRunning = false; super.onStop(); if (this.conversation != null) { this.conversation.setNextMessage(mEditMessage.getText().toString()); @@ -395,34 +511,6 @@ public class ConversationFragment extends Fragment { updateMessages(); } - private void decryptMessage(Message message) { - PgpEngine engine = activity.xmppConnectionService.getPgpEngine(); - if (engine != null) { - engine.decrypt(message, new UiCallback<Message>() { - - @Override - public void userInputRequried(PendingIntent pi, Message message) { - askForPassphraseIntent = pi.getIntentSender(); - showSnackbar(R.string.openpgp_messages_found, - R.string.decrypt, clickToDecryptListener); - } - - @Override - public void success(Message message) { - activity.xmppConnectionService.databaseBackend - .updateMessage(message); - updateMessages(); - } - - @Override - public void error(int error, Message message) { - message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); - // updateMessages(); - } - }); - } - } - public void updateMessages() { if (getView() == null) { return; @@ -458,13 +546,16 @@ public class ConversationFragment extends Fragment { }); } for (Message message : this.conversation.getMessages()) { - if ((message.getEncryption() == Message.ENCRYPTION_PGP) - && ((message.getStatus() == Message.STATUS_RECEIVED) || (message - .getStatus() == Message.STATUS_SEND))) { - decryptMessage(message); - break; + if (message.getEncryption() == Message.ENCRYPTION_PGP + && (message.getStatus() == Message.STATUS_RECEIVED || message + .getStatus() >= Message.STATUS_SEND) + && message.getDownloadable() == null) { + if (!mEncryptedMessages.contains(message)) { + mEncryptedMessages.add(message); + } } } + decryptNext(); this.messageList.clear(); if (this.conversation.getMessages().size() == 0) { messagesLoaded = false; @@ -476,7 +567,7 @@ public class ConversationFragment extends Fragment { this.messageListAdapter.notifyDataSetChanged(); if (conversation.getMode() == Conversation.MODE_SINGLE) { if (messageList.size() >= 1) { - makeFingerprintWarning(conversation.getLatestEncryption()); + makeFingerprintWarning(); } } else { if (!conversation.getMucOptions().online() @@ -522,6 +613,40 @@ public class ConversationFragment extends Fragment { } } + private void decryptNext() { + Message next = this.mEncryptedMessages.peek(); + PgpEngine engine = activity.xmppConnectionService.getPgpEngine(); + + if (next != null && engine != null && !mDecryptJobRunning) { + mDecryptJobRunning = true; + engine.decrypt(next, new UiCallback<Message>() { + + @Override + public void userInputRequried(PendingIntent pi, Message message) { + mDecryptJobRunning = false; + askForPassphraseIntent = pi.getIntentSender(); + showSnackbar(R.string.openpgp_messages_found, + R.string.decrypt, clickToDecryptListener); + } + + @Override + public void success(Message message) { + mDecryptJobRunning = false; + mEncryptedMessages.remove(); + activity.xmppConnectionService.updateMessage(message); + } + + @Override + public void error(int error, Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTION_FAILED); + mDecryptJobRunning = false; + mEncryptedMessages.remove(); + activity.xmppConnectionService.updateConversationUi(); + } + }); + } + } + private void messageSent() { int size = this.messageList.size(); messagesView.setSelection(size - 1); @@ -594,14 +719,13 @@ public class ConversationFragment extends Fragment { } } - protected void makeFingerprintWarning(int latestEncryption) { + protected void makeFingerprintWarning() { Set<String> knownFingerprints = conversation.getContact() .getOtrFingerprints(); - if ((latestEncryption == Message.ENCRYPTION_OTR) - && (conversation.hasValidOtrSession() + if (conversation.hasValidOtrSession() && (!conversation.isMuted()) && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints - .contains(conversation.getOtrFingerprint())))) { + .contains(conversation.getOtrFingerprint()))) { showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, new OnClickListener() { diff --git a/src/eu/siacs/conversations/ui/EditAccountActivity.java b/src/eu/siacs/conversations/ui/EditAccountActivity.java index 0ec38547..58ca49cc 100644 --- a/src/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/eu/siacs/conversations/ui/EditAccountActivity.java @@ -1,8 +1,6 @@ package eu.siacs.conversations.ui; import android.app.PendingIntent; -import android.content.ClipData; -import android.content.ClipboardManager; import android.content.Intent; import android.os.Bundle; import android.text.Editable; @@ -17,6 +15,7 @@ import android.widget.EditText; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.CompoundButton.OnCheckedChangeListener; +import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import eu.siacs.conversations.R; @@ -43,7 +42,7 @@ public class EditAccountActivity extends XmppActivity { private TextView mServerInfoPep; private TextView mSessionEst; private TextView mOtrFingerprint; - private TextView mOtrFingerprintHeadline; + private RelativeLayout mOtrFingerprintBox; private ImageButton mOtrFingerprintToClipboardButton; private String jidToEdit; @@ -277,7 +276,7 @@ public class EditAccountActivity extends XmppActivity { this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); - this.mOtrFingerprintHeadline = (TextView) findViewById(R.id.otr_fingerprint_headline); + this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); this.mSaveButton = (Button) findViewById(R.id.save_button); this.mCancelButton = (Button) findViewById(R.id.cancel_button); @@ -378,8 +377,7 @@ public class EditAccountActivity extends XmppActivity { final String fingerprint = this.mAccount .getOtrFingerprint(xmppConnectionService); if (fingerprint != null) { - this.mOtrFingerprintHeadline.setVisibility(View.VISIBLE); - this.mOtrFingerprint.setVisibility(View.VISIBLE); + this.mOtrFingerprintBox.setVisibility(View.VISIBLE); this.mOtrFingerprint.setText(fingerprint); this.mOtrFingerprintToClipboardButton .setVisibility(View.VISIBLE); @@ -389,7 +387,7 @@ public class EditAccountActivity extends XmppActivity { @Override public void onClick(View v) { - if (OtrFingerprintToClipBoard(fingerprint)) { + if (copyTextToClipboard(fingerprint,R.string.otr_fingerprint)) { Toast.makeText( EditAccountActivity.this, R.string.toast_message_otr_fingerprint, @@ -398,9 +396,7 @@ public class EditAccountActivity extends XmppActivity { } }); } else { - this.mOtrFingerprintToClipboardButton.setVisibility(View.GONE); - this.mOtrFingerprint.setVisibility(View.GONE); - this.mOtrFingerprintHeadline.setVisibility(View.GONE); + this.mOtrFingerprintBox.setVisibility(View.GONE); } } else { if (this.mAccount.errorStatus()) { @@ -411,15 +407,4 @@ public class EditAccountActivity extends XmppActivity { this.mStats.setVisibility(View.GONE); } } - - private boolean OtrFingerprintToClipBoard(String fingerprint) { - ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); - String label = getResources().getString(R.string.otr_fingerprint); - if (mClipBoardManager != null) { - ClipData mClipData = ClipData.newPlainText(label, fingerprint); - mClipBoardManager.setPrimaryClip(mClipData); - return true; - } - return false; - } } diff --git a/src/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/eu/siacs/conversations/ui/ManageAccountActivity.java index afe9e06e..77f8b68a 100644 --- a/src/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -70,7 +70,8 @@ public class ManageAccountActivity extends XmppActivity { public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - ManageAccountActivity.this.getMenuInflater().inflate(R.menu.manageaccounts_context, menu); + ManageAccountActivity.this.getMenuInflater().inflate( + R.menu.manageaccounts_context, menu); AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; this.selectedAccount = accountList.get(acmi.position); if (this.selectedAccount.isOptionSet(Account.OPTION_DISABLED)) { @@ -122,6 +123,7 @@ public class ManageAccountActivity extends XmppActivity { return true; case R.id.mgmt_account_announce_pgp: publishOpenPGPPublicKey(selectedAccount); + return true; default: return super.onContextItemSelected(item); } @@ -187,7 +189,8 @@ public class ManageAccountActivity extends XmppActivity { } private void deleteAccount(final Account account) { - AlertDialog.Builder builder = new AlertDialog.Builder(ManageAccountActivity.this); + AlertDialog.Builder builder = new AlertDialog.Builder( + ManageAccountActivity.this); builder.setTitle(getString(R.string.mgmt_account_are_you_sure)); builder.setIconAttribute(android.R.attr.alertDialogIcon); builder.setMessage(getString(R.string.mgmt_account_delete_confirm_text)); diff --git a/src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index f46d92f9..6aa40c41 100644 --- a/src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -158,8 +158,8 @@ public class PublishProfilePictureActivity extends XmppActivity { if (this.avatarUri == null) { if (this.account.getAvatar() != null || this.defaultUri == null) { - this.avatar.setImageBitmap(this.account.getImage( - getApplicationContext(), 384)); + this.avatar.setImageBitmap(avatarService().get(account, + getPixel(194))); if (this.defaultUri != null) { this.avatar .setOnLongClickListener(this.backToDefaultListener); diff --git a/src/eu/siacs/conversations/ui/StartConversationActivity.java b/src/eu/siacs/conversations/ui/StartConversationActivity.java index a1a2d4c2..416e926a 100644 --- a/src/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/eu/siacs/conversations/ui/StartConversationActivity.java @@ -463,11 +463,11 @@ public class StartConversationActivity extends XmppActivity { public boolean onCreateOptionsMenu(Menu menu) { this.mOptionsMenu = menu; getMenuInflater().inflate(R.menu.start_conversation, menu); - MenuItem menuCreateContact = (MenuItem) menu + MenuItem menuCreateContact = menu .findItem(R.id.action_create_contact); - MenuItem menuCreateConference = (MenuItem) menu + MenuItem menuCreateConference = menu .findItem(R.id.action_join_conference); - mMenuSearchView = (MenuItem) menu.findItem(R.id.action_search); + mMenuSearchView = menu.findItem(R.id.action_search); mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener); View mSearchView = mMenuSearchView.getActionView(); mSearchEditText = (EditText) mSearchView diff --git a/src/eu/siacs/conversations/ui/XmppActivity.java b/src/eu/siacs/conversations/ui/XmppActivity.java index cd77557c..222f3295 100644 --- a/src/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/eu/siacs/conversations/ui/XmppActivity.java @@ -12,6 +12,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.utils.ExceptionHelper; @@ -20,6 +21,8 @@ import android.app.Activity; import android.app.AlertDialog; import android.app.PendingIntent; import android.app.AlertDialog.Builder; +import android.content.ClipData; +import android.content.ClipboardManager; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -55,7 +58,6 @@ public abstract class XmppActivity extends Activity { public XmppConnectionService xmppConnectionService; public boolean xmppConnectionServiceBound = false; - protected boolean handledViewIntent = false; protected int mPrimaryTextColor; protected int mSecondaryTextColor; @@ -400,8 +402,7 @@ public abstract class XmppActivity extends Activity { private void quickEdit(final String previousValue, final OnValueEdited callback, boolean password) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - View view = (View) getLayoutInflater() - .inflate(R.layout.quickedit, null); + View view = getLayoutInflater().inflate(R.layout.quickedit, null); final EditText editor = (EditText) view.findViewById(R.id.editor); OnClickListener mClickListener = new OnClickListener() { @@ -448,7 +449,7 @@ public abstract class XmppActivity extends Activity { listener.onPresenceSelected(); } } else if (presences.size() == 1) { - String presence = (String) presences.asStringArray()[0]; + String presence = presences.asStringArray()[0]; conversation.setNextPresence(presence); listener.onPresenceSelected(); } else { @@ -526,6 +527,26 @@ public abstract class XmppActivity extends Activity { return this.mSecondaryBackgroundColor; } + public int getPixel(int dp) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + return ((int) (dp * metrics.density)); + } + + public boolean copyTextToClipboard(String text,int labelResId) { + ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + String label = getResources().getString(labelResId); + if (mClipBoardManager != null) { + ClipData mClipData = ClipData.newPlainText(label, text); + mClipBoardManager.setPrimaryClip(mClipData); + return true; + } + return false; + } + + public AvatarService avatarService() { + return xmppConnectionService.getAvatarService(); + } + class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { private final WeakReference<ImageView> imageViewReference; private Message message = null; diff --git a/src/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/eu/siacs/conversations/ui/adapter/AccountAdapter.java index 5c25bf34..e13b3204 100644 --- a/src/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -28,13 +28,14 @@ public class AccountAdapter extends ArrayAdapter<Account> { if (view == null) { LayoutInflater inflater = (LayoutInflater) getContext() .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = (View) inflater.inflate(R.layout.account_row, parent, false); + view = inflater.inflate(R.layout.account_row, parent, false); } TextView jid = (TextView) view.findViewById(R.id.account_jid); jid.setText(account.getJid()); TextView statusView = (TextView) view.findViewById(R.id.account_status); ImageView imageView = (ImageView) view.findViewById(R.id.account_image); - imageView.setImageBitmap(account.getImage(activity, 48)); + imageView.setImageBitmap(activity.avatarService().get(account, + activity.getPixel(48))); switch (account.getStatus()) { case Account.STATUS_DISABLED: statusView.setText(getContext().getString( diff --git a/src/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index f74856b0..b5c20dc5 100644 --- a/src/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -5,6 +5,7 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Downloadable; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.XmppActivity; @@ -34,14 +35,14 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { if (view == null) { LayoutInflater inflater = (LayoutInflater) activity .getSystemService(Context.LAYOUT_INFLATER_SERVICE); - view = (View) inflater.inflate(R.layout.conversation_list_row, + view = inflater.inflate(R.layout.conversation_list_row, parent, false); } - Conversation conv = getItem(position); + Conversation conversation = getItem(position); if (this.activity instanceof ConversationActivity) { ConversationActivity activity = (ConversationActivity) this.activity; if (!activity.isConversationsOverviewHideable()) { - if (conv == activity.getSelectedConversation()) { + if (conversation == activity.getSelectedConversation()) { view.setBackgroundColor(activity .getSecondaryBackgroundColor()); } else { @@ -53,65 +54,85 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { } TextView convName = (TextView) view .findViewById(R.id.conversation_name); - if (conv.getMode() == Conversation.MODE_SINGLE + if (conversation.getMode() == Conversation.MODE_SINGLE || activity.useSubjectToIdentifyConference()) { - convName.setText(conv.getName()); + convName.setText(conversation.getName()); } else { - convName.setText(conv.getContactJid().split("/")[0]); + convName.setText(conversation.getContactJid().split("/")[0]); } - TextView convLastMsg = (TextView) view + TextView mLastMessage = (TextView) view .findViewById(R.id.conversation_lastmsg); + TextView mTimestamp = (TextView) view + .findViewById(R.id.conversation_lastupdate); ImageView imagePreview = (ImageView) view .findViewById(R.id.conversation_lastimage); - Message latestMessage = conv.getLatestMessage(); + Message message = conversation.getLatestMessage(); - if (latestMessage.getType() == Message.TYPE_TEXT - || latestMessage.getType() == Message.TYPE_PRIVATE) { - if ((latestMessage.getEncryption() != Message.ENCRYPTION_PGP) - && (latestMessage.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED)) { - String body = Config.PARSE_EMOTICONS ? UIHelper - .transformAsciiEmoticons(latestMessage.getBody()) - : latestMessage.getBody(); - convLastMsg.setText(body); + if (!conversation.isRead()) { + convName.setTypeface(null, Typeface.BOLD); + } else { + convName.setTypeface(null, Typeface.NORMAL); + } + + if (message.getType() == Message.TYPE_IMAGE + || message.getDownloadable() != null) { + Downloadable d = message.getDownloadable(); + if (conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.ITALIC); } else { - convLastMsg.setText(R.string.encrypted_message_received); + mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC); } - 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); + if (d != null) { + mLastMessage.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); + 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 { - convLastMsg.setText(""); + 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); } - } - - if (!conv.isRead()) { - convName.setTypeface(null, Typeface.BOLD); - convLastMsg.setTypeface(null, Typeface.BOLD); } else { - convName.setTypeface(null, Typeface.NORMAL); - convLastMsg.setTypeface(null, Typeface.NORMAL); + 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); } - - ((TextView) view.findViewById(R.id.conversation_lastupdate)) - .setText(UIHelper.readableTimeDifference(getContext(), conv - .getLatestMessage().getTimeSent())); + mTimestamp.setText(UIHelper.readableTimeDifference(getContext(), + conversation.getLatestMessage().getTimeSent())); ImageView profilePicture = (ImageView) view .findViewById(R.id.conversation_image); - profilePicture.setImageBitmap(conv.getImage(activity, 56)); + profilePicture.setImageBitmap(activity.avatarService().get( + conversation, activity.getPixel(56))); return view; } diff --git a/src/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java b/src/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java index 0534bc25..143dfda1 100644 --- a/src/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java +++ b/src/eu/siacs/conversations/ui/adapter/KnownHostsAdapter.java @@ -47,11 +47,11 @@ public class KnownHostsAdapter extends ArrayAdapter<String> { @Override protected void publishResults(CharSequence constraint, FilterResults results) { - ArrayList<String> filteredList = (ArrayList<String>) results.values; + ArrayList filteredList = (ArrayList) results.values; if (results != null && results.count > 0) { clear(); - for (String c : filteredList) { - add(c); + for (Object c : filteredList) { + add((String) c); } notifyDataSetChanged(); } @@ -71,4 +71,4 @@ public class KnownHostsAdapter extends ArrayAdapter<String> { public Filter getFilter() { return domainFilter; } -}
\ No newline at end of file +} diff --git a/src/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index df67e566..efc6b4d9 100644 --- a/src/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -4,6 +4,7 @@ import java.util.List; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.ui.XmppActivity; import android.content.Context; import android.view.LayoutInflater; import android.view.View; @@ -14,8 +15,11 @@ import android.widget.TextView; public class ListItemAdapter extends ArrayAdapter<ListItem> { - public ListItemAdapter(Context context, List<ListItem> objects) { - super(context, 0, objects); + protected XmppActivity activity; + + public ListItemAdapter(XmppActivity activity, List<ListItem> objects) { + super(activity, 0, objects); + this.activity = activity; } @Override @@ -24,7 +28,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { .getSystemService(Context.LAYOUT_INFLATER_SERVICE); ListItem item = getItem(position); if (view == null) { - view = (View) inflater.inflate(R.layout.contact, parent, false); + view = inflater.inflate(R.layout.contact, parent, false); } TextView name = (TextView) view.findViewById(R.id.contact_display_name); TextView jid = (TextView) view.findViewById(R.id.contact_jid); @@ -32,7 +36,8 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { jid.setText(item.getJid()); name.setText(item.getDisplayName()); - picture.setImageBitmap(item.getImage(48, getContext())); + picture.setImageBitmap(activity.avatarService().get(item, + activity.getPixel(48))); return view; } diff --git a/src/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 2671cf50..a24f90d7 100644 --- a/src/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.ui.adapter; -import java.util.HashMap; import java.util.List; import eu.siacs.conversations.Config; @@ -9,11 +8,10 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Downloadable; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.Message.ImageParams; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.utils.UIHelper; -import android.content.Context; import android.content.Intent; -import android.graphics.Bitmap; import android.graphics.Typeface; import android.text.Spannable; import android.text.SpannableString; @@ -40,31 +38,26 @@ public class MessageAdapter extends ArrayAdapter<Message> { private ConversationActivity activity; - private Bitmap accountBitmap; - - private BitmapCache mBitmapCache = new BitmapCache(); private DisplayMetrics metrics; private OnContactPictureClicked mOnContactPictureClickedListener; private OnContactPictureLongClicked mOnContactPictureLongClickedListener; + private OnLongClickListener openContextMenu = new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + v.showContextMenu(); + return true; + } + }; + public MessageAdapter(ConversationActivity activity, List<Message> messages) { super(activity, 0, messages); this.activity = activity; metrics = getContext().getResources().getDisplayMetrics(); } - private Bitmap getSelfBitmap() { - if (this.accountBitmap == null) { - - if (getCount() > 0) { - this.accountBitmap = getItem(0).getConversation().getAccount() - .getImage(getContext(), 48); - } - } - return this.accountBitmap; - } - public void setOnContactPictureClicked(OnContactPictureClicked listener) { this.mOnContactPictureClickedListener = listener; } @@ -101,13 +94,14 @@ public class MessageAdapter extends ArrayAdapter<Message> { } boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI && message.getMergedStatus() <= Message.STATUS_RECEIVED; - if (message.getType() == Message.TYPE_IMAGE) { - String[] fileParams = message.getBody().split(","); - try { - long size = Long.parseLong(fileParams[0]); - filesize = size / 1024 + " KB"; - } catch (NumberFormatException e) { - filesize = "0 KB"; + if (message.getType() == Message.TYPE_IMAGE + || message.getDownloadable() != null) { + ImageParams params = message.getImageParams(); + if (params.size != 0) { + filesize = params.size / 1024 + " KB"; + } + if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) { + error = true; } } switch (message.getMergedStatus()) { @@ -134,14 +128,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { info = getContext().getString(R.string.send_failed); error = true; break; - case Message.STATUS_SEND_REJECTED: - info = getContext().getString(R.string.send_rejected); - error = true; - break; - case Message.STATUS_RECEPTION_FAILED: - info = getContext().getString(R.string.reception_failed); - error = true; - break; default: if (multiReceived) { Contact contact = message.getContact(); @@ -268,6 +254,22 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.messageBody.setTextIsSelectable(true); } + private void displayDownloadableMessage(ViewHolder viewHolder, + final Message message, int resid) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(resid); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + startDonwloadable(message); + } + }); + viewHolder.download_button.setOnLongClickListener(openContextMenu); + } + private void displayImageMessage(ViewHolder viewHolder, final Message message) { if (viewHolder.download_button != null) { @@ -275,23 +277,19 @@ public class MessageAdapter extends ArrayAdapter<Message> { } viewHolder.messageBody.setVisibility(View.GONE); viewHolder.image.setVisibility(View.VISIBLE); - String[] fileParams = message.getBody().split(","); - if (fileParams.length == 3) { - double target = metrics.density * 288; - int w = Integer.parseInt(fileParams[1]); - int h = Integer.parseInt(fileParams[2]); - int scalledW; - int scalledH; - if (w <= h) { - scalledW = (int) (w / ((double) h / target)); - scalledH = (int) target; - } else { - scalledW = (int) target; - scalledH = (int) (h / ((double) w / target)); - } - viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams( - scalledW, scalledH)); + ImageParams params = message.getImageParams(); + double target = metrics.density * 288; + int scalledW; + int scalledH; + if (params.width <= params.height) { + scalledW = (int) (params.width / ((double) params.height / target)); + scalledH = (int) target; + } else { + scalledW = (int) target; + scalledH = (int) (params.height / ((double) params.width / target)); } + viewHolder.image.setLayoutParams(new LinearLayout.LayoutParams( + scalledW, scalledH)); activity.loadBitmap(message, viewHolder.image); viewHolder.image.setOnClickListener(new OnClickListener() { @@ -303,23 +301,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { getContext().startActivity(intent); } }); - viewHolder.image.setOnLongClickListener(new OnLongClickListener() { - - @Override - public boolean onLongClick(View v) { - Intent shareIntent = new Intent(); - shareIntent.setAction(Intent.ACTION_SEND); - shareIntent.putExtra(Intent.EXTRA_STREAM, - activity.xmppConnectionService.getFileBackend() - .getJingleFileUri(message)); - shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - shareIntent.setType("image/webp"); - getContext().startActivity( - Intent.createChooser(shareIntent, - getContext().getText(R.string.share_with))); - return true; - } - }); + viewHolder.image.setOnLongClickListener(openContextMenu); } @Override @@ -331,17 +313,22 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder = new ViewHolder(); switch (type) { case NULL: - view = (View) activity.getLayoutInflater().inflate( + view = activity.getLayoutInflater().inflate( R.layout.message_null, parent, false); break; case SENT: - view = (View) activity.getLayoutInflater().inflate( + view = activity.getLayoutInflater().inflate( R.layout.message_sent, parent, false); viewHolder.message_box = (LinearLayout) view .findViewById(R.id.message_box); viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); - viewHolder.contact_picture.setImageBitmap(getSelfBitmap()); + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get( + item.getConversation().getAccount(), + activity.getPixel(48))); + viewHolder.download_button = (Button) view + .findViewById(R.id.download_button); viewHolder.indicator = (ImageView) view .findViewById(R.id.security_indicator); viewHolder.image = (ImageView) view @@ -355,21 +342,18 @@ public class MessageAdapter extends ArrayAdapter<Message> { view.setTag(viewHolder); break; case RECEIVED: - view = (View) activity.getLayoutInflater().inflate( + view = activity.getLayoutInflater().inflate( R.layout.message_received, parent, false); viewHolder.message_box = (LinearLayout) view .findViewById(R.id.message_box); viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); - viewHolder.download_button = (Button) view .findViewById(R.id.download_button); - if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { - - viewHolder.contact_picture.setImageBitmap(mBitmapCache.get( - item.getConversation().getContact(), getContext())); - + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(item.getContact(), + activity.getPixel(48))); } viewHolder.indicator = (ImageView) view .findViewById(R.id.security_indicator); @@ -382,15 +366,17 @@ public class MessageAdapter extends ArrayAdapter<Message> { view.setTag(viewHolder); break; case STATUS: - view = (View) activity.getLayoutInflater().inflate( + view = activity.getLayoutInflater().inflate( R.layout.message_status, parent, false); viewHolder.contact_picture = (ImageView) view .findViewById(R.id.message_photo); if (item.getConversation().getMode() == Conversation.MODE_SINGLE) { - viewHolder.contact_picture.setImageBitmap(mBitmapCache.get( - item.getConversation().getContact(), getContext())); - viewHolder.contact_picture.setAlpha(128); + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get( + item.getConversation().getContact(), + activity.getPixel(32))); + viewHolder.contact_picture.setAlpha(0.5f); viewHolder.contact_picture .setOnClickListener(new OnClickListener() { @@ -465,38 +451,40 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (item.getConversation().getMode() == Conversation.MODE_MULTI) { Contact contact = item.getContact(); if (contact != null) { - viewHolder.contact_picture.setImageBitmap(mBitmapCache.get( - contact, getContext())); + viewHolder.contact_picture.setImageBitmap(activity + .avatarService() + .get(contact, activity.getPixel(48))); } else { String name = item.getPresence(); if (name == null) { name = item.getCounterpart(); } - viewHolder.contact_picture.setImageBitmap(mBitmapCache.get( - name, getContext())); + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(name, activity.getPixel(48))); } } } - if (item.getType() == Message.TYPE_IMAGE) { - if (item.getStatus() == Message.STATUS_RECEIVING) { + if (item.getType() == Message.TYPE_IMAGE + || item.getDownloadable() != null) { + Downloadable d = item.getDownloadable(); + if (d != null && d.getStatus() == Downloadable.STATUS_DOWNLOADING) { displayInfoMessage(viewHolder, R.string.receiving_image); - } else if (item.getStatus() == Message.STATUS_RECEIVED_OFFER) { - viewHolder.image.setVisibility(View.GONE); - viewHolder.messageBody.setVisibility(View.GONE); - viewHolder.download_button.setVisibility(View.VISIBLE); - viewHolder.download_button - .setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - Downloadable downloadable = item - .getDownloadable(); - if (downloadable != null) { - downloadable.start(); - } - } - }); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_CHECKING) { + displayInfoMessage(viewHolder, R.string.checking_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_DELETED) { + displayInfoMessage(viewHolder, R.string.image_file_deleted); + } else if (d != null && d.getStatus() == Downloadable.STATUS_OFFER) { + displayDownloadableMessage(viewHolder, item, + R.string.download_image); + } else if (d != null + && d.getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE) { + displayDownloadableMessage(viewHolder, item, + R.string.check_image_filesize); + } else if (d != null && d.getStatus() == Downloadable.STATUS_FAILED) { + displayInfoMessage(viewHolder, R.string.image_transmission_failed); } else if ((item.getEncryption() == Message.ENCRYPTION_DECRYPTED) || (item.getEncryption() == Message.ENCRYPTION_NONE) || (item.getEncryption() == Message.ENCRYPTION_OTR)) { @@ -534,6 +522,16 @@ public class MessageAdapter extends ArrayAdapter<Message> { return view; } + public void startDonwloadable(Message message) { + Downloadable downloadable = message.getDownloadable(); + if (downloadable != null) { + if (!downloadable.start()) { + Toast.makeText(activity, R.string.not_connected_try_again, + Toast.LENGTH_SHORT).show(); + } + } + } + private static class ViewHolder { protected LinearLayout message_box; @@ -547,30 +545,6 @@ public class MessageAdapter extends ArrayAdapter<Message> { } - private class BitmapCache { - private HashMap<String, Bitmap> contactBitmaps = new HashMap<String, Bitmap>(); - private HashMap<String, Bitmap> unknownBitmaps = new HashMap<String, Bitmap>(); - - public Bitmap get(Contact contact, Context context) { - if (!contactBitmaps.containsKey(contact.getJid())) { - contactBitmaps.put(contact.getJid(), - contact.getImage(48, context)); - } - return contactBitmaps.get(contact.getJid()); - } - - public Bitmap get(String name, Context context) { - if (unknownBitmaps.containsKey(name)) { - return unknownBitmaps.get(name); - } else { - Bitmap bm = UIHelper - .getContactPicture(name, 48, context, false); - unknownBitmaps.put(name, bm); - return bm; - } - } - } - public interface OnContactPictureClicked { public void onContactPictureClicked(Message message); } diff --git a/src/eu/siacs/conversations/utils/CryptoHelper.java b/src/eu/siacs/conversations/utils/CryptoHelper.java index a28b519e..47595c6e 100644 --- a/src/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/eu/siacs/conversations/utils/CryptoHelper.java @@ -5,7 +5,6 @@ import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.Arrays; import eu.siacs.conversations.entities.Account; import android.util.Base64; @@ -28,9 +27,11 @@ public class CryptoHelper { } public static byte[] hexToBytes(String hexString) { - byte[] array = new BigInteger(hexString, 16).toByteArray(); - if (array[0] == 0) { - array = Arrays.copyOfRange(array, 1, array.length); + int len = hexString.length(); + byte[] array = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + array[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character + .digit(hexString.charAt(i + 1), 16)); } return array; } diff --git a/src/eu/siacs/conversations/utils/DNSHelper.java b/src/eu/siacs/conversations/utils/DNSHelper.java index c51a75ac..f101e883 100644 --- a/src/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/eu/siacs/conversations/utils/DNSHelper.java @@ -33,7 +33,7 @@ public class DNSHelper { for (String dnsserver : dns) { InetAddress ip = InetAddress.getByName(dnsserver); Bundle b = queryDNS(host, ip); - if (b.containsKey("name")) { + if (b.containsKey("values")) { return b; } else if (b.containsKey("error") && "nosrv".equals(b.getString("error", null))) { @@ -45,7 +45,7 @@ public class DNSHelper { } public static Bundle queryDNS(String host, InetAddress dnsServer) { - Bundle namePort = new Bundle(); + Bundle bundle = new Bundle(); try { String qname = "_xmpp-client._tcp." + host; Log.d(Config.LOGTAG, @@ -133,42 +133,28 @@ public class DNSHelper { } if (result.size() == 0) { - namePort.putString("error", "nosrv"); - return namePort; + bundle.putString("error", "nosrv"); + return bundle; } - // we now have a list of servers to try :-) - - // classic name/port pair - String resultName = result.get(0).getName(); - namePort.putString("name", resultName); - namePort.putInt("port", result.get(0).getPort()); - - if (ips4.containsKey(resultName)) { - // we have an ip! - ArrayList<String> ip = ips4.get(resultName); - Collections.shuffle(ip, rnd); - namePort.putString("ipv4", ip.get(0)); - } - if (ips6.containsKey(resultName)) { - ArrayList<String> ip = ips6.get(resultName); - Collections.shuffle(ip, rnd); - namePort.putString("ipv6", ip.get(0)); - } - - // add all other records - int i = 0; + ArrayList<Bundle> values = new ArrayList<Bundle>(); for (SRV srv : result) { - namePort.putString("name" + i, srv.getName()); - namePort.putInt("port" + i, srv.getPort()); - i++; + Bundle namePort = new Bundle(); + namePort.putString("name", srv.getName()); + namePort.putInt("port", srv.getPort()); + if (ips4.containsKey(srv.getName())) { + ArrayList<String> ip = ips4.get(srv.getName()); + Collections.shuffle(ip, rnd); + namePort.putString("ipv4", ip.get(0)); + } + values.add(namePort); } - + bundle.putParcelableArrayList("values", values); } catch (SocketTimeoutException e) { - namePort.putString("error", "timeout"); + bundle.putString("error", "timeout"); } catch (Exception e) { - namePort.putString("error", "unhandled"); + bundle.putString("error", "unhandled"); } - return namePort; + return bundle; } final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); diff --git a/src/eu/siacs/conversations/utils/ExifHelper.java b/src/eu/siacs/conversations/utils/ExifHelper.java new file mode 100644 index 00000000..ceda7293 --- /dev/null +++ b/src/eu/siacs/conversations/utils/ExifHelper.java @@ -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; + } + } +} diff --git a/src/eu/siacs/conversations/utils/PhoneHelper.java b/src/eu/siacs/conversations/utils/PhoneHelper.java index 25cff099..5becc7e7 100644 --- a/src/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/eu/siacs/conversations/utils/PhoneHelper.java @@ -22,7 +22,7 @@ public class PhoneHelper { final String[] PROJECTION = new String[] { ContactsContract.Data._ID, ContactsContract.Data.DISPLAY_NAME, - ContactsContract.Data.PHOTO_THUMBNAIL_URI, + ContactsContract.Data.PHOTO_URI, ContactsContract.Data.LOOKUP_KEY, ContactsContract.CommonDataKinds.Im.DATA }; @@ -50,10 +50,8 @@ public class PhoneHelper { "displayname", cursor.getString(cursor .getColumnIndex(ContactsContract.Data.DISPLAY_NAME))); - contact.putString( - "photouri", - cursor.getString(cursor - .getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI))); + contact.putString("photouri", cursor.getString(cursor + .getColumnIndex(ContactsContract.Data.PHOTO_URI))); contact.putString("lookup", cursor.getString(cursor .getColumnIndex(ContactsContract.Data.LOOKUP_KEY))); diff --git a/src/eu/siacs/conversations/utils/UIHelper.java b/src/eu/siacs/conversations/utils/UIHelper.java index 671e66d5..5141c83c 100644 --- a/src/eu/siacs/conversations/utils/UIHelper.java +++ b/src/eu/siacs/conversations/utils/UIHelper.java @@ -1,22 +1,18 @@ package eu.siacs.conversations.utils; -import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.List; -import java.util.Locale; import java.util.regex.Pattern; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.MucOptions.User; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ManageAccountActivity; import android.annotation.SuppressLint; -import android.app.Activity; import android.app.AlertDialog; import android.app.Notification; import android.app.NotificationManager; @@ -25,28 +21,15 @@ import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.net.Uri; -import android.provider.ContactsContract.Contacts; import android.support.v4.app.NotificationCompat; import android.support.v4.app.TaskStackBuilder; import android.text.format.DateFormat; import android.text.format.DateUtils; -import android.util.DisplayMetrics; import android.view.LayoutInflater; import android.view.View; -import android.widget.QuickContactBadge; import android.widget.TextView; public class UIHelper { - private static final int BG_COLOR = 0xFF181818; - private static final int FG_COLOR = 0xFFFAFAFA; - private static final int TRANSPARENT = 0x00000000; private static final int SHORT_DATE_FLAGS = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_YEAR | DateUtils.FORMAT_ABBREV_ALL; private static final int FULL_DATE_FLAGS = DateUtils.FORMAT_SHOW_TIME @@ -123,167 +106,6 @@ public class UIHelper { } } - public static int getRealPx(int dp, Context context) { - final DisplayMetrics metrics = context.getResources() - .getDisplayMetrics(); - return ((int) (dp * metrics.density)); - } - - private static int getNameColor(String name) { - /* - * int holoColors[] = { 0xFF1da9da, 0xFFb368d9, 0xFF83b600, 0xFFffa713, - * 0xFFe92727 }; - */ - int holoColors[] = { 0xFFe91e63, 0xFF9c27b0, 0xFF673ab7, 0xFF3f51b5, - 0xFF5677fc, 0xFF03a9f4, 0xFF00bcd4, 0xFF009688, 0xFFff5722, - 0xFF795548, 0xFF607d8b }; - return holoColors[(int) ((name.hashCode() & 0xffffffffl) % holoColors.length)]; - } - - private static void drawTile(Canvas canvas, String letter, int tileColor, - int textColor, int left, int top, int right, int bottom) { - Paint tilePaint = new Paint(), textPaint = new Paint(); - tilePaint.setColor(tileColor); - textPaint.setFlags(Paint.ANTI_ALIAS_FLAG); - textPaint.setColor(textColor); - textPaint.setTypeface(Typeface.create("sans-serif-light", - Typeface.NORMAL)); - textPaint.setTextSize((float) ((right - left) * 0.8)); - Rect rect = new Rect(); - - canvas.drawRect(new Rect(left, top, right, bottom), tilePaint); - textPaint.getTextBounds(letter, 0, 1, rect); - float width = textPaint.measureText(letter); - canvas.drawText(letter, (right + left) / 2 - width / 2, (top + bottom) - / 2 + rect.height() / 2, textPaint); - } - - private static Bitmap getUnknownContactPicture(String[] names, int size, - int bgColor, int fgColor) { - int tiles = (names.length > 4) ? 4 : (names.length < 1) ? 1 - : names.length; - Bitmap bitmap = Bitmap - .createBitmap(size, size, Bitmap.Config.ARGB_8888); - Canvas canvas = new Canvas(bitmap); - - String[] letters = new String[tiles]; - int[] colors = new int[tiles]; - if (names.length < 1) { - letters[0] = "?"; - colors[0] = 0xFFe92727; - } else { - for (int i = 0; i < tiles; ++i) { - letters[i] = (names[i].length() > 0) ? names[i].substring(0, 1) - .toUpperCase(Locale.US) : " "; - colors[i] = getNameColor(names[i]); - } - - if (names.length > 4) { - letters[3] = "\u2026"; // Unicode ellipsis - colors[3] = 0xFF202020; - } - } - - bitmap.eraseColor(bgColor); - - switch (tiles) { - case 1: - drawTile(canvas, letters[0], colors[0], fgColor, 0, 0, size, size); - break; - - case 2: - drawTile(canvas, letters[0], colors[0], fgColor, 0, 0, - size / 2 - 1, size); - drawTile(canvas, letters[1], colors[1], fgColor, size / 2 + 1, 0, - size, size); - break; - - case 3: - drawTile(canvas, letters[0], colors[0], fgColor, 0, 0, - size / 2 - 1, size); - drawTile(canvas, letters[1], colors[1], fgColor, size / 2 + 1, 0, - size, size / 2 - 1); - drawTile(canvas, letters[2], colors[2], fgColor, size / 2 + 1, - size / 2 + 1, size, size); - break; - - case 4: - drawTile(canvas, letters[0], colors[0], fgColor, 0, 0, - size / 2 - 1, size / 2 - 1); - drawTile(canvas, letters[1], colors[1], fgColor, 0, size / 2 + 1, - size / 2 - 1, size); - drawTile(canvas, letters[2], colors[2], fgColor, size / 2 + 1, 0, - size, size / 2 - 1); - drawTile(canvas, letters[3], colors[3], fgColor, size / 2 + 1, - size / 2 + 1, size, size); - break; - } - - return bitmap; - } - - private static Bitmap getMucContactPicture(Conversation conversation, - int size, int bgColor, int fgColor) { - List<User> members = conversation.getMucOptions().getUsers(); - if (members.size() == 0) { - return getUnknownContactPicture( - new String[] { conversation.getName() }, size, bgColor, - fgColor); - } - ArrayList<String> names = new ArrayList<String>(); - names.add(conversation.getMucOptions().getActualNick()); - for (User user : members) { - names.add(user.getName()); - if (names.size() > 4) { - break; - } - } - String[] mArrayNames = new String[names.size()]; - names.toArray(mArrayNames); - return getUnknownContactPicture(mArrayNames, size, bgColor, fgColor); - } - - public static Bitmap getContactPicture(Conversation conversation, - int dpSize, Context context, boolean notification) { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - return getContactPicture(conversation.getContact(), dpSize, - context, notification); - } else { - int fgColor = UIHelper.FG_COLOR, bgColor = (notification) ? UIHelper.BG_COLOR - : UIHelper.TRANSPARENT; - - return getMucContactPicture(conversation, - getRealPx(dpSize, context), bgColor, fgColor); - } - } - - public static Bitmap getContactPicture(Contact contact, int dpSize, - Context context, boolean notification) { - String uri = contact.getProfilePhoto(); - if (uri == null) { - return getContactPicture(contact.getDisplayName(), dpSize, context, - notification); - } - try { - Bitmap bm = BitmapFactory.decodeStream(context.getContentResolver() - .openInputStream(Uri.parse(uri))); - return Bitmap.createScaledBitmap(bm, getRealPx(dpSize, context), - getRealPx(dpSize, context), false); - } catch (FileNotFoundException e) { - return getContactPicture(contact.getDisplayName(), dpSize, context, - notification); - } - } - - public static Bitmap getContactPicture(String name, int dpSize, - Context context, boolean notification) { - int fgColor = UIHelper.FG_COLOR, bgColor = (notification) ? UIHelper.BG_COLOR - : UIHelper.TRANSPARENT; - - return getUnknownContactPicture(new String[] { name }, - getRealPx(dpSize, context), bgColor, fgColor); - } - public static void showErrorNotification(Context context, List<Account> accounts) { NotificationManager mNotificationManager = (NotificationManager) context @@ -326,16 +148,6 @@ public class UIHelper { mNotificationManager.notify(1111, notification); } - public static void prepareContactBadge(final Activity activity, - QuickContactBadge badge, final Contact contact, Context context) { - if (contact.getSystemAccount() != null) { - String[] systemAccount = contact.getSystemAccount().split("#"); - long id = Long.parseLong(systemAccount[0]); - badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1])); - } - badge.setImageBitmap(contact.getImage(72, context)); - } - @SuppressLint("InflateParams") public static AlertDialog getVerifyFingerprintDialog( final ConversationActivity activity, @@ -370,25 +182,6 @@ public class UIHelper { return builder.create(); } - public static Bitmap getSelfContactPicture(Account account, int size, - boolean showPhoneSelfContactPicture, Context context) { - if (showPhoneSelfContactPicture) { - Uri selfiUri = PhoneHelper.getSefliUri(context); - if (selfiUri != null) { - try { - return BitmapFactory.decodeStream(context - .getContentResolver().openInputStream(selfiUri)); - } catch (FileNotFoundException e) { - return getContactPicture(account.getJid(), size, context, - false); - } - } - return getContactPicture(account.getJid(), size, context, false); - } else { - return getContactPicture(account.getJid(), size, context, false); - } - } - private final static class EmoticonPattern { Pattern pattern; String replacement; diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java index 54409be4..39dcb362 100644 --- a/src/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/eu/siacs/conversations/xmpp/XmppConnection.java @@ -4,14 +4,17 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.math.BigInteger; +import java.net.InetSocketAddress; import java.net.Socket; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Hashtable; +import java.util.LinkedList; import java.util.List; import java.util.Map.Entry; @@ -22,14 +25,19 @@ import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.X509TrustManager; +import org.apache.http.conn.ssl.StrictHostnameVerifier; import org.xmlpull.v1.XmlPullParserException; import de.duenndns.ssl.MemorizingTrustManager; +import android.content.Context; +import android.content.SharedPreferences; import android.os.Bundle; +import android.os.Parcelable; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; +import android.preference.PreferenceManager; import android.util.Log; import android.util.SparseArray; import eu.siacs.conversations.Config; @@ -80,6 +88,7 @@ public class XmppConnection implements Runnable { private SparseArray<String> messageReceipts = new SparseArray<String>(); private boolean usingCompression = false; + private boolean usingEncryption = false; private int stanzasReceived = 0; private int stanzasSent = 0; @@ -104,6 +113,7 @@ public class XmppConnection implements Runnable { private OnBindListener bindListener = null; private OnMessageAcknowledged acknowledgedListener = null; private MemorizingTrustManager mMemorizingTrustManager; + private final Context applicationContext; public XmppConnection(Account account, XmppConnectionService service) { this.mRandom = service.getRNG(); @@ -112,6 +122,7 @@ public class XmppConnection implements Runnable { this.wakeLock = service.getPowerManager().newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, account.getJid()); tagWriter = new TagWriter(); + applicationContext = service.getApplicationContext(); } protected void changeStatus(int nextStatus) { @@ -135,6 +146,7 @@ public class XmppConnection implements Runnable { protected void connect() { Log.d(Config.LOGTAG, account.getJid() + ": connecting"); usingCompression = false; + usingEncryption = false; lastConnect = SystemClock.elapsedRealtime(); lastPingSent = SystemClock.elapsedRealtime(); this.attempt++; @@ -145,29 +157,47 @@ public class XmppConnection implements Runnable { tagWriter = new TagWriter(); packetCallbacks.clear(); this.changeStatus(Account.STATUS_CONNECTING); - Bundle namePort = DNSHelper.getSRVRecord(account.getServer()); - if ("timeout".equals(namePort.getString("error"))) { + Bundle result = DNSHelper.getSRVRecord(account.getServer()); + ArrayList<Parcelable> values = result.getParcelableArrayList("values"); + if ("timeout".equals(result.getString("error"))) { Log.d(Config.LOGTAG, account.getJid() + ": dns timeout"); this.changeStatus(Account.STATUS_OFFLINE); return; - } - String srvRecordServer = namePort.getString("name"); - String srvIpServer = namePort.getString("ipv4"); - int srvRecordPort = namePort.getInt("port"); - if (srvRecordServer != null) { - if (srvIpServer != null) { - Log.d(Config.LOGTAG, account.getJid() - + ": using values from dns " + srvRecordServer - + "[" + srvIpServer + "]:" + srvRecordPort); - socket = new Socket(srvIpServer, srvRecordPort); - } else { - Log.d(Config.LOGTAG, account.getJid() - + ": using values from dns " + srvRecordServer - + ":" + srvRecordPort); - socket = new Socket(srvRecordServer, srvRecordPort); - } - } else if (namePort.containsKey("error") - && "nosrv".equals(namePort.getString("error", null))) { + } else if (values != null) { + int i = 0; + boolean socketError = true; + while (socketError && values.size() > i) { + Bundle namePort = (Bundle) values.get(i); + try { + String srvRecordServer = namePort.getString("name"); + int srvRecordPort = namePort.getInt("port"); + String srvIpServer = namePort.getString("ipv4"); + InetSocketAddress addr; + if (srvIpServer!=null) { + addr = new InetSocketAddress(srvIpServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid() + + ": using values from dns " + srvRecordServer + + "[" + srvIpServer + "]:" + srvRecordPort); + } else { + addr = new InetSocketAddress(srvRecordServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid() + + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + } + socket = new Socket(); + socket.connect(addr, 20000); + socketError = false; + } catch (UnknownHostException e) { + i++; + } catch (IOException e) { + i++; + } + } + if (socketError) { + throw new IOException(); + } + } else if (result.containsKey("error") + && "nosrv".equals(result.getString("error", null))) { socket = new Socket(account.getServer(), 5222); } else { Log.d(Config.LOGTAG, account.getJid() @@ -504,6 +534,15 @@ public class XmppConnection implements Runnable { tagWriter.writeTag(startTLS); } + private SharedPreferences getPreferences() { + return PreferenceManager + .getDefaultSharedPreferences(applicationContext); + } + + private boolean enableLegacySSL() { + return getPreferences().getBoolean("enable_legacy_ssl", false); + } + private void switchOverToTls(Tag currentTag) throws XmlPullParserException, IOException { tagReader.readTag(); @@ -515,11 +554,27 @@ public class XmppConnection implements Runnable { SSLSocketFactory factory = sc.getSocketFactory(); HostnameVerifier verifier = this.mMemorizingTrustManager - .wrapHostnameVerifier(new org.apache.http.conn.ssl.StrictHostnameVerifier()); + .wrapHostnameVerifier(new StrictHostnameVerifier()); SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket, socket.getInetAddress().getHostAddress(), socket.getPort(), true); + // Support all protocols except legacy SSL. + // The min SDK version prevents us having to worry about SSLv2. In + // future, this may be + // true of SSLv3 as well. + final String[] supportProtocols; + if (enableLegacySSL()) { + supportProtocols = sslSocket.getSupportedProtocols(); + } else { + final List<String> supportedProtocols = new LinkedList<String>( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = new String[supportedProtocols.size()]; + supportedProtocols.toArray(supportProtocols); + } + sslSocket.setEnabledProtocols(supportProtocols); + if (verifier != null && !verifier.verify(account.getServer(), sslSocket.getSession())) { @@ -533,6 +588,7 @@ public class XmppConnection implements Runnable { sendStartStream(); Log.d(Config.LOGTAG, account.getJid() + ": TLS connection established"); + usingEncryption = true; processStream(tagReader.readTag()); sslSocket.close(); } catch (NoSuchAlgorithmException e1) { @@ -562,20 +618,20 @@ public class XmppConnection implements Runnable { private void processStreamFeatures(Tag currentTag) throws XmlPullParserException, IOException { this.streamFeatures = tagReader.readElement(currentTag); - if (this.streamFeatures.hasChild("starttls") - && account.isOptionSet(Account.OPTION_USETLS)) { + if (this.streamFeatures.hasChild("starttls") && !usingEncryption) { sendStartTLS(); } else if (compressionAvailable()) { sendCompressionZlib(); } else if (this.streamFeatures.hasChild("register") - && (account.isOptionSet(Account.OPTION_REGISTER))) { + && account.isOptionSet(Account.OPTION_REGISTER) + && usingEncryption) { sendRegistryRequest(); } else if (!this.streamFeatures.hasChild("register") - && (account.isOptionSet(Account.OPTION_REGISTER))) { + && account.isOptionSet(Account.OPTION_REGISTER)) { changeStatus(Account.STATUS_REGISTRATION_NOT_SUPPORTED); disconnect(true); } else if (this.streamFeatures.hasChild("mechanisms") - && shouldAuthenticate) { + && shouldAuthenticate && usingEncryption) { List<String> mechanisms = extractMechanisms(streamFeatures .findChild("mechanisms")); if (mechanisms.contains("PLAIN")) { @@ -591,6 +647,10 @@ public class XmppConnection implements Runnable { this.tagWriter.writeStanzaAsync(resume); } else if (this.streamFeatures.hasChild("bind") && shouldBind) { sendBindRequest(); + } else { + Log.d(Config.LOGTAG, account.getJid() + + ": incompatible server. disconnecting"); + disconnect(true); } } @@ -910,7 +970,7 @@ public class XmppConnection implements Runnable { } public void disconnect(boolean force) { - Log.d(Config.LOGTAG, account.getJid()+": disconnecting"); + Log.d(Config.LOGTAG, account.getJid() + ": disconnecting"); try { if (force) { socket.close(); diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 92fdbe0b..6b9ca9aa 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -16,6 +16,7 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Downloadable; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; @@ -33,17 +34,18 @@ public class JingleConnection implements Downloadable { private JingleConnectionManager mJingleConnectionManager; private XmppConnectionService mXmppConnectionService; - public static final int STATUS_INITIATED = 0; - public static final int STATUS_ACCEPTED = 1; - public static final int STATUS_TERMINATED = 2; - public static final int STATUS_CANCELED = 3; - public static final int STATUS_FINISHED = 4; - public static final int STATUS_TRANSMITTING = 5; - public static final int STATUS_FAILED = 99; + protected static final int JINGLE_STATUS_INITIATED = 0; + protected static final int JINGLE_STATUS_ACCEPTED = 1; + protected static final int JINGLE_STATUS_TERMINATED = 2; + protected static final int JINGLE_STATUS_CANCELED = 3; + protected static final int JINGLE_STATUS_FINISHED = 4; + protected static final int JINGLE_STATUS_TRANSMITTING = 5; + protected static final int JINGLE_STATUS_FAILED = 99; private int ibbBlockSize = 4096; - private int status = -1; + private int mJingleStatus = -1; + private int mStatus = -1; private Message message; private String sessionId; private Account account; @@ -54,7 +56,7 @@ public class JingleConnection implements Downloadable { private String transportId; private Element fileOffer; - private JingleFile file = null; + private DownloadableFile file = null; private String contentName; private String contentCreator; @@ -71,11 +73,7 @@ public class JingleConnection implements Downloadable { @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() == IqPacket.TYPE_ERROR) { - if (initiator.equals(account.getFullJid())) { - mXmppConnectionService.markMessage(message, - Message.STATUS_SEND_FAILED); - } - status = STATUS_FAILED; + cancel(); } } }; @@ -83,7 +81,7 @@ public class JingleConnection implements Downloadable { final OnFileTransmissionStatusChanged onFileTransmissionSatusChanged = new OnFileTransmissionStatusChanged() { @Override - public void onFileTransmitted(JingleFile file) { + public void onFileTransmitted(DownloadableFile file) { if (responder.equals(account.getFullJid())) { sendSuccess(); if (acceptedAutomatically) { @@ -96,8 +94,8 @@ public class JingleConnection implements Downloadable { BitmapFactory.decodeFile(file.getAbsolutePath(), options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; - message.setBody(Long.toString(file.getSize()) + ',' - + imageWidth + ',' + imageHeight); + message.setBody(Long.toString(file.getSize()) + '|' + + imageWidth + '|' + imageHeight); mXmppConnectionService.databaseBackend.createMessage(message); mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVED); @@ -148,8 +146,8 @@ public class JingleConnection implements Downloadable { return this.sessionId; } - public String getAccountJid() { - return this.account.getFullJid(); + public Account getAccount() { + return this.account; } public String getCounterPart() { @@ -253,13 +251,14 @@ public class JingleConnection implements Downloadable { } public void init(Account account, JinglePacket packet) { - this.status = STATUS_INITIATED; + this.mJingleStatus = JINGLE_STATUS_INITIATED; Conversation conversation = this.mXmppConnectionService .findOrCreateConversation(account, packet.getFrom().split("/", 2)[0], false); this.message = new Message(conversation, "", Message.ENCRYPTION_NONE); + this.message.setStatus(Message.STATUS_RECEIVED); this.message.setType(Message.TYPE_IMAGE); - this.message.setStatus(Message.STATUS_RECEIVED_OFFER); + this.mStatus = Downloadable.STATUS_OFFER; this.message.setDownloadable(this); String[] fromParts = packet.getFrom().split("/", 2); this.message.setPresence(fromParts[1]); @@ -304,7 +303,8 @@ public class JingleConnection implements Downloadable { if (supportedFile) { long size = Long.parseLong(fileSize.getContent()); message.setBody(Long.toString(size)); - conversation.getMessages().add(message); + conversation.add(message); + mXmppConnectionService.updateConversationUi(); if (size <= this.mJingleConnectionManager .getAutoAcceptFileSize()) { Log.d(Config.LOGTAG, "auto accepting file from " @@ -323,7 +323,7 @@ public class JingleConnection implements Downloadable { .push(message); } this.file = this.mXmppConnectionService.getFileBackend() - .getJingleFile(message, false); + .getFile(message, false); if (message.getEncryption() == Message.ENCRYPTION_OTR) { byte[] key = conversation.getSymmetricKey(); if (key == null) { @@ -350,12 +350,13 @@ public class JingleConnection implements Downloadable { } private void sendInitRequest() { + this.mXmppConnectionService.markMessage(this.message, Message.STATUS_OFFERED); JinglePacket packet = this.bootstrapPacket("session-initiate"); Content content = new Content(this.contentCreator, this.contentName); if (message.getType() == Message.TYPE_IMAGE) { content.setTransportId(this.transportId); - this.file = this.mXmppConnectionService.getFileBackend() - .getJingleFile(message, false); + this.file = this.mXmppConnectionService.getFileBackend().getFile( + message, false); if (message.getEncryption() == Message.ENCRYPTION_OTR) { Conversation conversation = this.message.getConversation(); this.mXmppConnectionService.renewSymmetricKey(conversation); @@ -369,7 +370,7 @@ public class JingleConnection implements Downloadable { content.socks5transport().setChildren(getCandidatesAsElements()); packet.setContent(content); this.sendJinglePacket(packet); - this.status = STATUS_INITIATED; + this.mJingleStatus = JINGLE_STATUS_INITIATED; } } @@ -382,8 +383,9 @@ public class JingleConnection implements Downloadable { } private void sendAccept() { - status = STATUS_ACCEPTED; - mXmppConnectionService.markMessage(message, Message.STATUS_RECEIVING); + mJingleStatus = JINGLE_STATUS_ACCEPTED; + this.mStatus = Downloadable.STATUS_DOWNLOADING; + mXmppConnectionService.updateConversationUi(); this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() { @@ -457,7 +459,7 @@ public class JingleConnection implements Downloadable { Content content = packet.getJingleContent(); mergeCandidates(JingleCandidate.parse(content.socks5transport() .getChildren())); - this.status = STATUS_ACCEPTED; + this.mJingleStatus = JINGLE_STATUS_ACCEPTED; mXmppConnectionService.markMessage(message, Message.STATUS_UNSEND); this.connectNextCandidate(); return true; @@ -492,7 +494,8 @@ public class JingleConnection implements Downloadable { } else if (content.socks5transport().hasChild("candidate-error")) { Log.d(Config.LOGTAG, "received candidate error"); this.receivedCandidate = true; - if ((status == STATUS_ACCEPTED) && (this.sentCandidate)) { + if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) + && (this.sentCandidate)) { this.connect(); } return true; @@ -504,7 +507,8 @@ public class JingleConnection implements Downloadable { JingleCandidate candidate = getCandidate(cid); candidate.flagAsUsedByCounterpart(); this.receivedCandidate = true; - if ((status == STATUS_ACCEPTED) && (this.sentCandidate)) { + if ((mJingleStatus == JINGLE_STATUS_ACCEPTED) + && (this.sentCandidate)) { this.connect(); } else { Log.d(Config.LOGTAG, @@ -532,7 +536,7 @@ public class JingleConnection implements Downloadable { this.sendFallbackToIbb(); } } else { - this.status = STATUS_TRANSMITTING; + this.mJingleStatus = JINGLE_STATUS_TRANSMITTING; if (connection.needsActivation()) { if (connection.getCandidate().isOurs()) { Log.d(Config.LOGTAG, "candidate " @@ -619,13 +623,15 @@ public class JingleConnection implements Downloadable { packet.setReason(reason); this.sendJinglePacket(packet); this.disconnect(); - this.status = STATUS_FINISHED; - this.mXmppConnectionService.markMessage(this.message, - Message.STATUS_RECEIVED); + this.mJingleStatus = JINGLE_STATUS_FINISHED; + this.message.setStatus(Message.STATUS_RECEIVED); + this.message.setDownloadable(null); + this.mXmppConnectionService.updateMessage(message); this.mJingleConnectionManager.finishConnection(this); } private void sendFallbackToIbb() { + Log.d(Config.LOGTAG, "sending fallback to ibb"); JinglePacket packet = this.bootstrapPacket("transport-replace"); Content content = new Content(this.contentCreator, this.contentName); this.transportId = this.mJingleConnectionManager.nextRandomId(); @@ -637,6 +643,7 @@ public class JingleConnection implements Downloadable { } private boolean receiveFallbackToIbb(JinglePacket packet) { + Log.d(Config.LOGTAG, "receiving fallack to ibb"); String receivedBlockSize = packet.getJingleContent().ibbTransport() .getAttribute("block-size"); if (receivedBlockSize != null) { @@ -691,7 +698,7 @@ public class JingleConnection implements Downloadable { } private void receiveSuccess() { - this.status = STATUS_FINISHED; + this.mJingleStatus = JINGLE_STATUS_FINISHED; this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND); this.disconnect(); @@ -699,20 +706,15 @@ public class JingleConnection implements Downloadable { } public void cancel() { - this.status = STATUS_CANCELED; + this.mJingleStatus = JINGLE_STATUS_CANCELED; this.disconnect(); if (this.message != null) { if (this.responder.equals(account.getFullJid())) { - this.mXmppConnectionService.markMessage(this.message, - Message.STATUS_RECEPTION_FAILED); + this.mStatus = Downloadable.STATUS_FAILED; + this.mXmppConnectionService.updateConversationUi(); } else { - if (this.status == STATUS_INITIATED) { - this.mXmppConnectionService.markMessage(this.message, - Message.STATUS_SEND_REJECTED); - } else { - this.mXmppConnectionService.markMessage(this.message, - Message.STATUS_SEND_FAILED); - } + this.mXmppConnectionService.markMessage(this.message, + Message.STATUS_SEND_FAILED); } } this.mJingleConnectionManager.finishConnection(this); @@ -789,7 +791,7 @@ public class JingleConnection implements Downloadable { .setAttribute("cid", cid); packet.setContent(content); this.sentCandidate = true; - if ((receivedCandidate) && (status == STATUS_ACCEPTED)) { + if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { connect(); } this.sendJinglePacket(packet); @@ -802,7 +804,7 @@ public class JingleConnection implements Downloadable { content.socks5transport().addChild("candidate-error"); packet.setContent(content); this.sentCandidate = true; - if ((receivedCandidate) && (status == STATUS_ACCEPTED)) { + if ((receivedCandidate) && (mJingleStatus == JINGLE_STATUS_ACCEPTED)) { connect(); } this.sendJinglePacket(packet); @@ -816,8 +818,8 @@ public class JingleConnection implements Downloadable { return this.responder; } - public int getStatus() { - return this.status; + public int getJingleStatus() { + return this.mJingleStatus; } private boolean equalCandidateExists(JingleCandidate candidate) { @@ -867,17 +869,34 @@ public class JingleConnection implements Downloadable { return this.transport; } - public void start() { - if (status == STATUS_INITIATED) { - new Thread(new Runnable() { + public boolean start() { + if (account.getStatus() == Account.STATUS_ONLINE) { + if (mJingleStatus == JINGLE_STATUS_INITIATED) { + new Thread(new Runnable() { - @Override - public void run() { - sendAccept(); - } - }).start(); + @Override + public void run() { + sendAccept(); + } + }).start(); + } + return true; + } else { + return false; + } + } + + @Override + public int getStatus() { + return this.mStatus; + } + + @Override + public long getFileSize() { + if (this.file != null) { + return this.file.getExpectedSize(); } else { - Log.d(Config.LOGTAG, "status (" + status + ") was not ok"); + return 0; } } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 79090af6..d937146a 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -10,16 +10,14 @@ import android.util.Log; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; import eu.siacs.conversations.xmpp.stanzas.IqPacket; -public class JingleConnectionManager { - - private XmppConnectionService xmppConnectionService; - +public class JingleConnectionManager extends AbstractConnectionManager { private List<JingleConnection> connections = new CopyOnWriteArrayList<JingleConnection>(); private HashMap<String, JingleCandidate> primaryCandidates = new HashMap<String, JingleCandidate>(); @@ -28,7 +26,7 @@ public class JingleConnectionManager { private SecureRandom random = new SecureRandom(); public JingleConnectionManager(XmppConnectionService service) { - this.xmppConnectionService = service; + super(service); } public void deliverPacket(Account account, JinglePacket packet) { @@ -38,7 +36,7 @@ public class JingleConnectionManager { connections.add(connection); } else { for (JingleConnection connection : connections) { - if (connection.getAccountJid().equals(account.getFullJid()) + if (connection.getAccount() == account && connection.getSessionId().equals( packet.getSessionId()) && connection.getCounterPart().equals(packet.getFrom())) { @@ -46,8 +44,13 @@ public class JingleConnectionManager { return; } } - account.getXmppConnection().sendIqPacket( - packet.generateRespone(IqPacket.TYPE_ERROR), null); + IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); + Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("item-not-found", + "urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("unknown-session", "urn:xmpp:jingle:errors:1"); + account.getXmppConnection().sendIqPacket(response, null); } } @@ -68,10 +71,6 @@ public class JingleConnectionManager { this.connections.remove(connection); } - public XmppConnectionService getXmppConnectionService() { - return this.xmppConnectionService; - } - public void getPrimaryCandidate(Account account, final OnPrimaryCandidateFound listener) { if (!this.primaryCandidates.containsKey(account.getJid())) { @@ -128,16 +127,6 @@ public class JingleConnectionManager { return new BigInteger(50, random).toString(32); } - public long getAutoAcceptFileSize() { - String config = this.xmppConnectionService.getPreferences().getString( - "auto_accept_file_size", "524288"); - try { - return Long.parseLong(config); - } catch (NumberFormatException e) { - return 524288; - } - } - public void deliverIbbPacket(Account account, IqPacket packet) { String sid = null; Element payload = null; @@ -152,7 +141,8 @@ public class JingleConnectionManager { } if (sid != null) { for (JingleConnection connection : connections) { - if (connection.hasTransportId(sid)) { + if (connection.getAccount() == account + && connection.hasTransportId(sid)) { JingleTransport transport = connection.getTransport(); if (transport instanceof JingleInbandTransport) { JingleInbandTransport inbandTransport = (JingleInbandTransport) transport; @@ -170,7 +160,7 @@ public class JingleConnectionManager { public void cancelInTransmission() { for (JingleConnection connection : this.connections) { - if (connection.getStatus() == JingleConnection.STATUS_TRANSMITTING) { + if (connection.getJingleStatus() == JingleConnection.JINGLE_STATUS_TRANSMITTING) { connection.cancel(); } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java b/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java deleted file mode 100644 index 9253814b..00000000 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleFile.java +++ /dev/null @@ -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; - } -} diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index c5498075..cc1e92f6 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -1,6 +1,5 @@ package eu.siacs.conversations.xmpp.jingle; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -10,6 +9,7 @@ import java.util.Arrays; import android.util.Base64; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; @@ -26,7 +26,7 @@ public class JingleInbandTransport extends JingleTransport { private boolean established = false; - private JingleFile file; + private DownloadableFile file; private InputStream fileInputStream = null; private OutputStream fileOutputStream; @@ -77,7 +77,7 @@ public class JingleInbandTransport extends JingleTransport { } @Override - public void receive(JingleFile file, + public void receive(DownloadableFile file, OnFileTransmissionStatusChanged callback) { this.onFileTransmissionStatusChanged = callback; this.file = file; @@ -86,7 +86,7 @@ public class JingleInbandTransport extends JingleTransport { digest.reset(); file.getParentFile().mkdirs(); file.createNewFile(); - this.fileOutputStream = getOutputStream(file); + this.fileOutputStream = file.createOutputStream(); if (this.fileOutputStream == null) { callback.onFileTransferAborted(); return; @@ -100,20 +100,19 @@ public class JingleInbandTransport extends JingleTransport { } @Override - public void send(JingleFile file, OnFileTransmissionStatusChanged callback) { + public void send(DownloadableFile file, + OnFileTransmissionStatusChanged callback) { this.onFileTransmissionStatusChanged = callback; this.file = file; try { this.digest = MessageDigest.getInstance("SHA-1"); this.digest.reset(); - fileInputStream = this.getInputStream(file); + fileInputStream = this.file.createInputStream(); if (fileInputStream == null) { callback.onFileTransferAborted(); return; } this.sendNextBlock(); - } catch (FileNotFoundException e) { - callback.onFileTransferAborted(); } catch (NoSuchAlgorithmException e) { callback.onFileTransferAborted(); } diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java index 63f5a507..1da2f0cd 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleSocks5Transport.java @@ -10,6 +10,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.utils.CryptoHelper; public class JingleSocks5Transport extends JingleTransport { @@ -29,11 +30,11 @@ public class JingleSocks5Transport extends JingleTransport { StringBuilder destBuilder = new StringBuilder(); destBuilder.append(jingleConnection.getSessionId()); if (candidate.isOurs()) { - destBuilder.append(jingleConnection.getAccountJid()); + destBuilder.append(jingleConnection.getAccount().getFullJid()); destBuilder.append(jingleConnection.getCounterPart()); } else { destBuilder.append(jingleConnection.getCounterPart()); - destBuilder.append(jingleConnection.getAccountJid()); + destBuilder.append(jingleConnection.getAccount().getFullJid()); } mDigest.reset(); this.destination = CryptoHelper.bytesToHex(mDigest @@ -86,7 +87,7 @@ public class JingleSocks5Transport extends JingleTransport { } - public void send(final JingleFile file, + public void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { new Thread(new Runnable() { @@ -96,7 +97,7 @@ public class JingleSocks5Transport extends JingleTransport { try { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.reset(); - fileInputStream = getInputStream(file); + fileInputStream = file.createInputStream(); if (fileInputStream == null) { callback.onFileTransferAborted(); return; @@ -132,7 +133,7 @@ public class JingleSocks5Transport extends JingleTransport { } - public void receive(final JingleFile file, + public void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback) { new Thread(new Runnable() { @@ -145,7 +146,7 @@ public class JingleSocks5Transport extends JingleTransport { socket.setSoTimeout(30000); file.getParentFile().mkdirs(); file.createNewFile(); - OutputStream fileOutputStream = getOutputStream(file); + OutputStream fileOutputStream = file.createOutputStream(); if (fileOutputStream == null) { callback.onFileTransferAborted(); return; diff --git a/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java b/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java index 07dc8ecc..1374e61c 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java +++ b/src/eu/siacs/conversations/xmpp/jingle/JingleTransport.java @@ -1,88 +1,13 @@ package eu.siacs.conversations.xmpp.jingle; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; - -import javax.crypto.Cipher; -import javax.crypto.CipherOutputStream; -import javax.crypto.CipherInputStream; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; - -import eu.siacs.conversations.Config; - -import android.util.Log; +import eu.siacs.conversations.entities.DownloadableFile; public abstract class JingleTransport { public abstract void connect(final OnTransportConnected callback); - public abstract void receive(final JingleFile file, + public abstract void receive(final DownloadableFile file, final OnFileTransmissionStatusChanged callback); - public abstract void send(final JingleFile file, + public abstract void send(final DownloadableFile file, final OnFileTransmissionStatusChanged callback); - - private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, - 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; - - protected InputStream getInputStream(JingleFile file) - throws FileNotFoundException { - if (file.getKey() == null) { - return new FileInputStream(file); - } else { - try { - IvParameterSpec ips = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, file.getKey(), ips); - Log.d(Config.LOGTAG, "opening encrypted input stream"); - return new CipherInputStream(new FileInputStream(file), cipher); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); - return null; - } catch (NoSuchPaddingException e) { - Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); - return null; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); - return null; - } catch (InvalidAlgorithmParameterException e) { - Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); - return null; - } - } - } - - protected OutputStream getOutputStream(JingleFile file) - throws FileNotFoundException { - if (file.getKey() == null) { - return new FileOutputStream(file); - } else { - try { - IvParameterSpec ips = new IvParameterSpec(iv); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.DECRYPT_MODE, file.getKey(), ips); - Log.d(Config.LOGTAG, "opening encrypted output stream"); - return new CipherOutputStream(new FileOutputStream(file), - cipher); - } catch (NoSuchAlgorithmException e) { - Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); - return null; - } catch (NoSuchPaddingException e) { - Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); - return null; - } catch (InvalidKeyException e) { - Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); - return null; - } catch (InvalidAlgorithmParameterException e) { - Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); - return null; - } - } - } } diff --git a/src/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java b/src/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java index 19fd4d97..e45e7441 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java +++ b/src/eu/siacs/conversations/xmpp/jingle/OnFileTransmissionStatusChanged.java @@ -1,7 +1,9 @@ package eu.siacs.conversations.xmpp.jingle; +import eu.siacs.conversations.entities.DownloadableFile; + public interface OnFileTransmissionStatusChanged { - public void onFileTransmitted(JingleFile file); + public void onFileTransmitted(DownloadableFile file); public void onFileTransferAborted(); } diff --git a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java index d19e6dfd..bcadbe77 100644 --- a/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java +++ b/src/eu/siacs/conversations/xmpp/jingle/stanzas/Content.java @@ -1,7 +1,7 @@ package eu.siacs.conversations.xmpp.jingle.stanzas; +import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jingle.JingleFile; public class Content extends Element { @@ -25,7 +25,7 @@ public class Content extends Element { this.transportId = sid; } - public void setFileOffer(JingleFile actualFile, boolean otr) { + public void setFileOffer(DownloadableFile actualFile, boolean otr) { Element description = this.addChild("description", "urn:xmpp:jingle:apps:file-transfer:3"); Element offer = description.addChild("offer"); |