diff options
46 files changed, 1013 insertions, 376 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 29277eb4..6b109baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ ###Changelog +####Version 0.8 +* Download HTTP images +* Show avatars in MUC tiles +* Disabled SSLv3 +* Performance improvments +* bug fixes + ####Version 0.7.3 * revised tablet ui * internal rewrites diff --git a/docs/MISSION.md b/docs/MISSION.md new file mode 100644 index 00000000..74399e74 --- /dev/null +++ b/docs/MISSION.md @@ -0,0 +1,25 @@ +Conversations is a messenger for the next decade. Based on already established +internet standards that have been around for over ten years Coversations isn’t +trying to replace current commercial messengers. It will simply outlive them. +Commercial, closed source products are coming and going. 15 years ago we had +ICQ which was replaced by Skype. MySpace was replaced by Facebook. WhatsApp and +Hangouts will disapear soon. Internet standards however stick around. People are +still using IRC and e-mail even though these protocols have been around for +decades. Utilizing proven standards doesn’t mean one can not evolve. GMail has +revolutionized the way we look at e-mail. Firefox and Chrome have changed the +way we use the Web. Conversations will change the way we look at instant +messaging. Being less obstrusive than a telephone call instant messaging has +always played an importent role in modern society. Conversations will show that +instant messaging can be fast, relialbe and private. Conversations will not +force its security and privacey aspects upon the user. For those willing to use encryption +Conversations will make it as uncomplicated as possible. However Conversations +is aware that end-to-end encryption by the very principle isn’t trivial. Instead +of trying the impossible and making encryption easier than comparing a +fingerprint Conversations will try to educate the willing user and explain the +necessary steps and the reasons behind them. Those unwilling to learn about +encryption will still be protected by the design principals of Conversations. +Conversations will simply not share or generate certain information for example +by encouraging the use of federated servers. Conversations will always +utilize the best available standards for encryption and media encoding instead +of reinventing the wheel. However it isn’t afraid to break with behavior patterns +that have been proven ineffctive. diff --git a/docs/XEPs.md b/docs/XEPs.md new file mode 100644 index 00000000..0dd6a1d7 --- /dev/null +++ b/docs/XEPs.md @@ -0,0 +1,18 @@ +* XEP-0027: Current Jabber OpenPGP Usage +* XEP-0030: Service Discovery +* XEP-0045: Multi-User Chat +* XEP-0048: Bookmarks +* XEP-0115: Entity Capabilities +* XEP-0138: Stream Compression +* XEP-0163: Personal Eventing Protocol (avatars and nicks) +* XEP-0166: Jingle (only used for file transfer) +* XEP-0184: Message Delivery Receipts (reply only) +* XEP-0198: Stream Management +* XEP-0234: Jingle File Transfer +* XEP-0237: Roster Versioning +* XEP-0249: Direct MUC Invitations (receiving only) +* XEP-0260: Jingle SOCKS5 Bytestreams Transport Method +* XEP-0261: Jingle In-Band Bytestreams Transport Method +* XEP-0280: Message Carbons +* XEP-0333: Chat Markers +* XEP-0352: Client State Indication diff --git a/docs/obeservations.md b/docs/obeservations.md new file mode 100644 index 00000000..f4e4bf17 --- /dev/null +++ b/docs/obeservations.md @@ -0,0 +1,97 @@ +Observations on implementing XMPP +================================= +After spending the last two and a half month basically writing my own XMPP +library from scratch I decided to share some of the observations I made in the +process.. In part this article can be seen as a response to a blog post made by +Dr. Ing. Georg Lukas. The blog post introduces a couple of XEP (XMPP Extensions) +which make the life on mobile devices a lot easier but states that they are +currently very few implementations of those XEPs. So I went ahead and +implemented all of them in my Android XMPP client. + +###General observations +The first thing I noticed is that XMPP is actually okish designed. If you were +to design a new chat protocol today you probably wouldn’t choose XML again +however the protocol basically consists of only three different packages which +are quickly hidden under some sort of abstraction layer within your library. +Getting from zero to sending messages to other users actually was very simple +and straight forward. But then came the XEPs. + +###Multi-User Chat +The first one was XEP-0045 Multi-User Chat. This is the one XEP of the XEPs I’m +going to mention in my article which is actually wildly adopted. Most clients +and servers I know of support MUC. However the level of completeness varies. +MUC actually introduces access and permission roles which are far more complex +than what some of us are used to from IRC but a lot of clients just don’t +implement them. I’m not implementing them myself (at least for now) because I +somewhat doubt that someone would actually use them. (How ever this might be +some sort of chicken or egg problem.) I did find some strange bugs though which +might be interesting for other library developers. In theory a MUC server +implementation can allow a single user (same jid) to join a conference room +multiple times with the same nick from different clients. This means if someone +wants to participate in a conference from two different devices (mobile and +desktop for example) one wouldn’t have to name oneself userDesktop and +userMobile but just user. Both ejabberd and prosody support this but with +strange side effects. prosody for example doesn’t allow a user to change its +name once two clients are “merged” by having the same nick. + +###Carbons and Stream Management +Two of the other XEPs Lukas’ mentions - Carbons (XEP-0280) and Stream Management +(XEP-0198) - were actually fairly easy to implement. The only challenges were to +find a server to support them (I ended up running my own prosody server) and a +desktop client to test them with. For carbons there is a patched mcabber version +and gajim. After implementing stream management I had very good results on my +mobile device. I had sessions running for up to 24 hours with a walking outside, +loosing mobile coverage for a few minutes and so on. The only limitation was +that I had to keep on developing and reinstalling my app. + +###Off the record +And then came OTR... This is were I spend the most time debugging stuff and +trying to get things right and compatible with other clients. This is the part +were I want to help other developers not to make the same mistakes and maybe +come to some sort of consent among XMPP developers to ultimately increase the +interoperability. OTR has some down sides which make it difficult or at times +even dangerous to implement within XMPP. First of all it is a synchronous +protocol which is tunneled through a different protocol (XMPP). Synchronous +means - among other things - auto replies. (An OTR session begins with “hi I’m +speaking otr give me your key” “ok cool here is my key”) And auto replies - we +know that since the first time an out of office auto responder went postal - are +dangerous. Things really start to get messy when you use one of the best +features of XMPP - multiple clients. The way XMPP works is that clients are +encouraged to send their messages to the raw jid and let the server decide what +full jid the messages are routed to. If in doubt even all of them. So what +happens when Alice sends a start-otr-message to Bobs raw jid? Bob receives the +message on his notebook as well as his cell phone. Both of them answer. Alice +gets two different replies. Shit explodes. Even if Alice sends the message to +bob/notebook chances are that Bob has carbon messages enabled and still receives +the messages on both devices. Now assuming that Bobs client is clever enough not +to auto reply to carbonated messages Bob/cellphone will still end up with a lot +of garbage messages. (Essentially the entire conversation between Alice and +Bob/notebook but unreadable of course) Therefor it should be good practice to +tag OTR messages as both private and no-copy. (private is part of the carbons +XEP, no-copy is a general hint. I found that prosody for some reasons doesn’t +honor the private tag on outgoing messages. While this is easily fixed I presume +that having both the private and the no-copy tag will make it more compatible +with servers or clients I don’t know about yet) + +####Rules to follow when implementing OTR +To summarize my observations on implementing OTR in XMPP let me make the +following three statements. + +1. While it is good practice for unencrypted messages to be send to the raw jid +and have the receiving server or user decide how they should be routed OTR +messages must be send to a specific resource. To make this work the user should +be given the option to select the presence (which can be assisted with some +educated guessing by the client based on previous messages). +Furthermore a client should encourage a user to choose meaningful presences +instead of the clients name or even random ones. Something like /mobile, +/notebook, /desktop is a greater assist to any one who wants to start an otr +session then /Gajim, /mcabber or /pidgin + +2. Messages should be tagged private and no-copy to avoid unnecessary traffic or +otr error loops with faulty clients. This tagging should be done even if your +own client doesn’t support carbons. + +3. When dealing with “legacy clients” - meaning clients which don’t follow my +advise a client should be extra careful not to create message loops. This means +to not respond with otr errors if a client is not 100% sure it is the only +client which received the message diff --git a/proguard-project.txt b/proguard-project.txt new file mode 100644 index 00000000..f2fe1559 --- /dev/null +++ b/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 1725eca6..7dd5a799 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -11,6 +11,7 @@ public final class Config { public static final int PING_TIMEOUT = 10; public static final int CONNECT_TIMEOUT = 90; 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/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 2696c7d2..9a2b4a11 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -8,13 +8,12 @@ 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; @@ -26,7 +25,7 @@ import eu.siacs.conversations.ui.UiCallback; 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,10 +76,6 @@ public class PgpEngine { message); return; case OpenPgpApi.RESULT_CODE_ERROR: - OpenPgpError error = result - .getParcelableExtra(OpenPgpApi.RESULT_ERROR); - Log.d(Config.LOGTAG, - "openpgp error: " + error.getMessage()); callback.error(R.string.openpgp_error, message); return; default: @@ -101,18 +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); - ; + 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: @@ -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/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index a390c7ca..8a83c465 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -5,9 +5,7 @@ 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 { @@ -18,7 +16,6 @@ public class Message extends AbstractEntity { 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; @@ -145,19 +142,6 @@ public class Message extends AbstractEntity { return body; } - public String getReadableBody(Context context) { - if (encryption == ENCRYPTION_PGP) { - return context.getText(R.string.encrypted_message_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; } @@ -342,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() { @@ -379,13 +365,13 @@ 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() { - Contact contact = this.getContact(); - if (status <= STATUS_RECEIVED - && (contact == null || !contact.trusted())) { - return false; - } try { URL url = new URL(this.getBody()); if (!url.getProtocol().equalsIgnoreCase("http") @@ -396,7 +382,12 @@ public class Message extends AbstractEntity { return false; } String[] pathParts = url.getPath().split("/"); - String filename = pathParts[pathParts.length - 1]; + 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( @@ -418,19 +409,28 @@ public class Message extends AbstractEntity { } public ImageParams getImageParams() { - ImageParams params = new ImageParams(); + 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(","); + 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 { @@ -451,6 +451,11 @@ public class Message extends AbstractEntity { } 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; @@ -468,8 +473,37 @@ public class Message extends AbstractEntity { } 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; diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/src/main/java/eu/siacs/conversations/http/HttpConnection.java index 407a13d9..0810e167 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnection.java @@ -20,9 +20,7 @@ import org.apache.http.conn.ssl.StrictHostnameVerifier; import android.content.Intent; import android.graphics.BitmapFactory; import android.net.Uri; -import android.util.Log; -import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Downloadable; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; @@ -38,6 +36,7 @@ public class HttpConnection implements Downloadable { private Message message; private DownloadableFile file; private int mStatus = Downloadable.STATUS_UNKNOWN; + private boolean acceptedAutomatically = false; public HttpConnection(HttpConnectionManager manager) { this.mHttpConnectionManager = manager; @@ -63,12 +62,19 @@ public class HttpConnection implements Downloadable { 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); @@ -95,6 +101,10 @@ public class HttpConnection implements Downloadable { mXmppConnectionService.sendBroadcast(intent); message.setDownloadable(null); mHttpConnectionManager.finishConnection(this); + mXmppConnectionService.updateConversationUi(); + if (acceptedAutomatically) { + mXmppConnectionService.getNotificationService().push(message); + } } private void changeStatus(int status) { @@ -147,6 +157,8 @@ public class HttpConnection implements Downloadable { 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(); @@ -154,9 +166,12 @@ public class HttpConnection implements Downloadable { } 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); } } @@ -231,8 +246,8 @@ public class HttpConnection implements Downloadable { BitmapFactory.decodeFile(file.getAbsolutePath(), options); int imageHeight = options.outHeight; int imageWidth = options.outWidth; - message.setBody(mUrl.toString() + "," + file.getSize() + ',' - + imageWidth + ',' + imageHeight); + message.setBody(mUrl.toString() + "|" + file.getSize() + '|' + + imageWidth + '|' + imageHeight); message.setType(Message.TYPE_IMAGE); mXmppConnectionService.updateMessage(message); } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index b5e14305..383ac89a 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -256,7 +256,6 @@ public class MessageParser extends AbstractParser implements return null; } } - return finishedMessage; } @@ -478,15 +477,24 @@ public class MessageParser extends AbstractParser implements } Conversation conversation = message.getConversation(); 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.bodyContainsDownloadable()) { + if (message.trusted() && message.bodyContainsDownloadable()) { this.mXmppConnectionService.getHttpConnectionManager() .createNewConnection(message); + notify = false; } notify = notify && !conversation.isMuted(); if (notify) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index b49cf4e6..12e5e251 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -332,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/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index b891e9ef..13daa27b 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -20,7 +20,6 @@ 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; @@ -29,11 +28,11 @@ import android.util.Base64OutputStream; import android.util.Log; 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.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.ExifHelper; import eu.siacs.conversations.xmpp.pep.Avatar; public class FileBackend { @@ -181,23 +180,12 @@ 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; } } } @@ -226,27 +214,6 @@ public class FileBackend { return thumbnail; } - public void removeFiles(Conversation conversation) { - String prefix = mXmppConnectionService.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 diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index c0668a19..bd27b555 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -4,7 +4,6 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; -import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; @@ -17,7 +16,6 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; import android.net.Uri; -import android.util.Log; public class AvatarService { @@ -43,7 +41,6 @@ public class AvatarService { if (avatar != null) { return avatar; } - Log.d(Config.LOGTAG, "no cache hit for " + KEY); avatar = mXmppConnectionService.getFileBackend().getAvatar( contact.getAvatar(), size); if (avatar == null) { @@ -116,7 +113,6 @@ public class AvatarService { if (bitmap != null) { return bitmap; } - Log.d(Config.LOGTAG, "no cache hit for " + KEY); List<MucOptions.User> users = mucOptions.getUsers(); int count = users.size(); bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); @@ -178,7 +174,6 @@ public class AvatarService { if (avatar != null) { return avatar; } - Log.d(Config.LOGTAG, "no cache hit for " + KEY); avatar = mXmppConnectionService.getFileBackend().getAvatar( account.getAvatar(), size); if (avatar == null) { @@ -211,7 +206,6 @@ public class AvatarService { if (bitmap != null) { return bitmap; } - Log.d(Config.LOGTAG, "no cache hit for " + KEY); bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); String letter = name.substring(0, 1); diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 00765deb..7b2e16df 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/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,16 +12,22 @@ 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; @@ -30,9 +37,10 @@ public class NotificationService { 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 mLastNotification; public NotificationService(XmppConnectionService service) { this.mXmppConnectionService = service; @@ -58,7 +66,8 @@ public class NotificationService { } Account account = message.getConversation().getAccount(); updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) - && !account.inGracePeriod()); + && !account.inGracePeriod() + && !this.inMiniGracePeriod(account)); } } @@ -89,74 +98,14 @@ public class NotificationService { if (notifications.size() == 0) { 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(mXmppConnectionService - .getAvatarService().get(conversation, getPixel(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 { - notificationManager.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) { @@ -168,6 +117,7 @@ public class NotificationService { mBuilder.setSound(Uri.parse(ringtone)); } } + mBuilder.setSmallIcon(R.drawable.ic_notification); mBuilder.setDeleteIntent(createDeleteIntent()); mBuilder.setLights(0xffffffff, 2000, 4000); Notification notification = mBuilder.build(); @@ -175,6 +125,142 @@ public class NotificationService { } } + 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(); + } + } + private PendingIntent createContentIntent(String conversationUuid) { TaskStackBuilder stackBuilder = TaskStackBuilder .create(mXmppConnectionService); @@ -234,4 +320,14 @@ public class NotificationService { .getDisplayMetrics(); return ((int) (dp * metrics.density)); } + + private void markLastNotification() { + this.mLastNotification = SystemClock.elapsedRealtime(); + } + + 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/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 37e334eb..be73e07f 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -127,7 +127,11 @@ public class XmppConnectionService extends Service { 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); } @@ -567,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 { @@ -590,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) { @@ -922,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; } } @@ -991,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(); } @@ -1924,4 +1929,21 @@ public class XmppConnectionService extends Service { } } + + 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/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java index 62a2cbe1..f14da352 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/main/java/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/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 6b4642cb..52687c81 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -227,7 +227,7 @@ public class ConferenceDetailsActivity extends XmppActivity { LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); membersView.removeAllViews(); 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); diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index ae26466e..4c52c609 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -325,8 +325,7 @@ public class ContactDetailsActivity extends XmppActivity { .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 @@ -345,8 +344,7 @@ public class ContactDetailsActivity extends XmppActivity { } if (contact.getPgpKeyId() != 0) { hasKeys = true; - 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); keyType.setText("PGP Key ID"); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 91e1c81f..1d7364d6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -191,6 +191,7 @@ public class ConversationActivity extends XmppActivity implements xmppConnectionService.getNotificationService() .setOpenConversation(null); } + closeContextMenu(); } @Override @@ -240,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()) { @@ -628,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); } } @@ -694,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; @@ -702,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() @@ -731,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)) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0e71801b..20eeeb30 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -33,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; @@ -45,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; @@ -193,6 +198,7 @@ public class ConversationFragment extends Fragment { }; private ConversationActivity activity; + private Message selectedMessage; private void sendMessage() { if (this.conversation == null) { @@ -326,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); @@ -435,9 +546,10 @@ 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))) { + 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); } @@ -455,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() @@ -607,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/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 1543d740..58ca49cc 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/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; @@ -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, @@ -409,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/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java index 5b5b0608..77f8b68a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -123,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); } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index a1a2d4c2..416e926a 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/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/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index d26f0e31..222f3295 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -21,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; @@ -56,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; @@ -401,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() { @@ -449,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 { @@ -531,6 +531,17 @@ public abstract class XmppActivity extends Activity { 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(); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java index 4ca21a3b..e13b3204 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java @@ -28,7 +28,7 @@ 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()); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index 183c89fa..b5c20dc5 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -35,7 +35,7 @@ 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 conversation = getItem(position); @@ -78,14 +78,14 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { if (message.getType() == Message.TYPE_IMAGE || message.getDownloadable() != null) { Downloadable d = message.getDownloadable(); + if (conversation.isRead()) { + mLastMessage.setTypeface(null, Typeface.ITALIC); + } else { + mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC); + } if (d != null) { mLastMessage.setVisibility(View.VISIBLE); imagePreview.setVisibility(View.GONE); - if (conversation.isRead()) { - mLastMessage.setTypeface(null, Typeface.ITALIC); - } else { - mLastMessage.setTypeface(null, Typeface.BOLD_ITALIC); - } if (d.getStatus() == Downloadable.STATUS_CHECKING) { mLastMessage.setText(R.string.checking_image); } else if (d.getStatus() == Downloadable.STATUS_DOWNLOADING) { @@ -99,6 +99,10 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { } else { mLastMessage.setText(""); } + } else if (message.getEncryption() == Message.ENCRYPTION_PGP) { + imagePreview.setVisibility(View.GONE); + mLastMessage.setVisibility(View.VISIBLE); + mLastMessage.setText(R.string.encrypted_message_received); } else { mLastMessage.setVisibility(View.GONE); imagePreview.setVisibility(View.VISIBLE); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index 977aa7b5..efc6b4d9 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -28,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); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index a9a55cbf..a24f90d7 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -43,6 +43,15 @@ public class MessageAdapter extends ArrayAdapter<Message> { 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; @@ -91,6 +100,9 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (params.size != 0) { filesize = params.size / 1024 + " KB"; } + if (message.getDownloadable() != null && message.getDownloadable().getStatus() == Downloadable.STATUS_FAILED) { + error = true; + } } switch (message.getMergedStatus()) { case Message.STATUS_WAITING: @@ -116,10 +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; default: if (multiReceived) { Contact contact = message.getContact(); @@ -259,6 +267,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { startDonwloadable(message); } }); + viewHolder.download_button.setOnLongClickListener(openContextMenu); } private void displayImageMessage(ViewHolder viewHolder, @@ -292,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 @@ -320,11 +313,11 @@ 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); @@ -349,7 +342,7 @@ 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); @@ -373,7 +366,7 @@ 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); @@ -490,6 +483,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { && 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)) { diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index c51a75ac..f101e883 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/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/main/java/eu/siacs/conversations/utils/ExifHelper.java b/src/main/java/eu/siacs/conversations/utils/ExifHelper.java new file mode 100644 index 00000000..ceda7293 --- /dev/null +++ b/src/main/java/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/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 903dc59d..39dcb362 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -4,6 +4,7 @@ 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; @@ -32,6 +33,7 @@ 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; @@ -155,50 +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 { + } else if (values != null) { + int i = 0; boolean socketError = true; - int srvIndex = 0; - while (socketError - && namePort.containsKey("name" + srvIndex)) { + while (socketError && values.size() > i) { + Bundle namePort = (Bundle) values.get(i); try { - srvRecordServer = namePort.getString("name" - + srvIndex); - srvRecordPort = namePort.getInt("port" + srvIndex); - Log.d(Config.LOGTAG, account.getJid() - + ": using values from dns " - + srvRecordServer + ":" + srvRecordPort); - socket = new Socket(srvRecordServer, srvRecordPort); + 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) { - srvIndex++; - if (!namePort.containsKey("name" + srvIndex)) { - throw e; - } + i++; } catch (IOException e) { - srvIndex++; - if (!namePort.containsKey("name" + srvIndex)) { - throw e; - } + i++; } } - } - } else if (namePort.containsKey("error") - && "nosrv".equals(namePort.getString("error", null))) { + 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() diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index a0b2feb2..6b9ca9aa 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -73,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); - } - mJingleStatus = JINGLE_STATUS_FAILED; + cancel(); } } }; @@ -98,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); @@ -354,6 +350,7 @@ 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) { @@ -716,13 +713,8 @@ public class JingleConnection implements Downloadable { this.mStatus = Downloadable.STATUS_FAILED; this.mXmppConnectionService.updateConversationUi(); } else { - if (this.mJingleStatus == JINGLE_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); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 1e7c84d4..d937146a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -44,8 +44,13 @@ public class JingleConnectionManager extends AbstractConnectionManager { 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); } } diff --git a/src/main/res/drawable/infocard_border.xml b/src/main/res/drawable/infocard_border.xml index af7d5d22..7c7ded57 100644 --- a/src/main/res/drawable/infocard_border.xml +++ b/src/main/res/drawable/infocard_border.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="UTF-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" > <solid android:color="@color/primarybackground" /> diff --git a/src/main/res/drawable/message_border.xml b/src/main/res/drawable/message_border.xml index b35693d5..b463d788 100644 --- a/src/main/res/drawable/message_border.xml +++ b/src/main/res/drawable/message_border.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="UTF-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" > diff --git a/src/main/res/drawable/snackbar.xml b/src/main/res/drawable/snackbar.xml index 13818618..951d7aee 100644 --- a/src/main/res/drawable/snackbar.xml +++ b/src/main/res/drawable/snackbar.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="UTF-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android" > <solid android:color="@color/darkbackground" /> diff --git a/src/main/res/layout/message_received.xml b/src/main/res/layout/message_received.xml index 730d00d5..39bb842a 100644 --- a/src/main/res/layout/message_received.xml +++ b/src/main/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/src/main/res/layout/message_sent.xml b/src/main/res/layout/message_sent.xml index e3e9b673..3e854643 100644 --- a/src/main/res/layout/message_sent.xml +++ b/src/main/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,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/src/main/res/menu/message_context.xml b/src/main/res/menu/message_context.xml new file mode 100644 index 00000000..80d4d196 --- /dev/null +++ b/src/main/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/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 185c5d31..6c68fe75 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -255,6 +255,11 @@ <string name="pref_use_larger_font">Zvětšit velikost písma</string> <string name="pref_use_larger_font_summary">Použít větší písmo v celé aplikaci</string> <string name="pref_use_send_button_to_indicate_status">Tlačítko pro odeslání zobrazuje stav</string> + <string name="pref_use_indicate_received">Požadovat oznámení o přijetí</string> + <string name="pref_use_indicate_received_summary">Přijaté zprávy budou označeny zeleně. Toto nemusí vždy plně fungovat.</string> <string name="pref_use_send_button_to_indicate_status_summary">Obarvit tlačítko pro odeslání barvou stavu kontaktu</string> + <string name="pref_expert_options_other">Další</string> + <string name="pref_conference_name">Jméno konference</string> + <string name="pref_conference_name_summary">Pro identifikaci konferencí použít téma místnosti místo jejího JID</string> </resources> diff --git a/src/main/res/values-de/arrays.xml b/src/main/res/values-de/arrays.xml index 9b429c5a..ed5d47b5 100644 --- a/src/main/res/values-de/arrays.xml +++ b/src/main/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/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 72121774..ca190deb 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/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/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 7fdc95c0..47424d00 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/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/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index 43c141ea..8896e9b5 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -1,4 +1,4 @@ -<?xml version="1.0" encoding="utf-8"?> +<?xml version="1.0" encoding="UTF-8"?> <resources> <string name="app_name">Conversations</string> diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index 1a4fd25d..4acc9e62 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/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/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 3862bb7b..e941ed6d 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -249,10 +249,10 @@ <string name="pref_force_encryption">Force end-to-end encryption</string> <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_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 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> @@ -272,5 +272,15 @@ <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 |