aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md7
-rw-r--r--docs/MISSION.md25
-rw-r--r--docs/XEPs.md18
-rw-r--r--docs/obeservations.md97
-rw-r--r--proguard-project.txt20
-rw-r--r--src/main/java/eu/siacs/conversations/Config.java1
-rw-r--r--src/main/java/eu/siacs/conversations/crypto/PgpEngine.java45
-rw-r--r--src/main/java/eu/siacs/conversations/entities/Message.java84
-rw-r--r--src/main/java/eu/siacs/conversations/http/HttpConnection.java23
-rw-r--r--src/main/java/eu/siacs/conversations/parser/MessageParser.java12
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java18
-rw-r--r--src/main/java/eu/siacs/conversations/persistance/FileBackend.java45
-rw-r--r--src/main/java/eu/siacs/conversations/services/AvatarService.java6
-rw-r--r--src/main/java/eu/siacs/conversations/services/NotificationService.java232
-rw-r--r--src/main/java/eu/siacs/conversations/services/XmppConnectionService.java34
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java2
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java2
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java6
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationActivity.java59
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ConversationFragment.java127
-rw-r--r--src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java15
-rw-r--r--src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java1
-rw-r--r--src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java6
-rw-r--r--src/main/java/eu/siacs/conversations/ui/XmppActivity.java19
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/AccountAdapter.java2
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java16
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java2
-rw-r--r--src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java45
-rw-r--r--src/main/java/eu/siacs/conversations/utils/DNSHelper.java50
-rw-r--r--src/main/java/eu/siacs/conversations/utils/ExifHelper.java161
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java67
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java20
-rw-r--r--src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java9
-rw-r--r--src/main/res/drawable/infocard_border.xml2
-rw-r--r--src/main/res/drawable/message_border.xml2
-rw-r--r--src/main/res/drawable/snackbar.xml2
-rw-r--r--src/main/res/layout/message_received.xml4
-rw-r--r--src/main/res/layout/message_sent.xml4
-rw-r--r--src/main/res/menu/message_context.xml20
-rw-r--r--src/main/res/values-cs/strings.xml5
-rw-r--r--src/main/res/values-de/arrays.xml2
-rw-r--r--src/main/res/values-de/strings.xml30
-rw-r--r--src/main/res/values-es/strings.xml16
-rw-r--r--src/main/res/values-eu/strings.xml2
-rw-r--r--src/main/res/values/arrays.xml4
-rw-r--r--src/main/res/values/strings.xml20
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&#8230;</string>
<string name="waiting">warten&#8230;</string>
<string name="no_pgp_key">Kein OpenPGP-Schlüssel gefunden</string>
- <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string>
+ <string name="contact_has_no_pgp_key">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge OpenPGP einrichten.</small></string>
<string name="no_pgp_keys">Keine OpenPGP-Schlüssel gefunden</string>
- <string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil dein Kontakt seinen oder ihren Schlüssel nicht preisgibt.\n\n<small>Bitte sag deinem Kontakt, er oder sie möge bitte OpenPGP einrichten.</small></string>
+ <string name="contacts_have_no_pgp_keys">Conversations ist nicht in der Lage, deine Nachrichten zu verschlüsseln, weil deine Kontakte ihre Schlüssel nicht preisgeben.\n\n<small>Bitte sag deinen Kontakten, sie mögen OpenPGP einrichten.</small></string>
<string name="encrypted_message_received"><i>Verschlüsselte Nachricht erhalten. Drücke hier, um sie anzuzeigen und zu entschlüsseln.</i></string>
<string name="encrypted_image_received"><i>Verschlüsseltes Bild erhalten. Drücke hier, um es anzuzeigen und zu entschlüsseln.</i></string>
<string name="image_file"><i>Bild erhalten. Drücke hier, um es anzuzeigen.</i></string>
@@ -100,7 +100,7 @@
<string name="pref_sound">Klingelton</string>
<string name="pref_sound_summary">Spiele Klingelton, wenn eine neue Nachricht ankommt</string>
<string name="pref_conference_notifications">Konferenz-Benachrichtigungen</string>
- <string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde.</string>
+ <string name="pref_conference_notifications_summary">Benachrichtige mich bei jeder Konferenznachricht und nicht nur, wenn ich angesprochen werde</string>
<string name="pref_notification_grace_period">Gnadenfrist</string>
<string name="pref_notification_grace_period_summary">Deaktiviere Benachrichtigungen für eine kurze Zeit nach Erhalt einer Nachricht, die von einem anderen deiner Clients kommt.</string>
<string name="pref_advanced_options">Erweiterte Optionen</string>
@@ -233,7 +233,7 @@
<string name="skip">Überspringen</string>
<string name="pref_ui_options">Benutzeroberfläche</string>
<string name="pref_use_indicate_received">Anfrage für Nachrichten Empfang</string>
- <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte das dies nicht unbedingt in allen Fällen funktioniert.</string>
+ <string name="pref_use_indicate_received_summary">Empfangene Nachrichten werden mit einem grünen Häckchen markiert. Bitte beachte, dass dies nicht in allen Fällen funktioniert.</string>
<string name="disable_notifications">Benachrichtigungen deaktivieren</string>
<string name="disable_notifications_for_this_conversation">Benachrichtigungen für diese Unterhaltung deaktivieren</string>
<string name="notifications_disabled">Benachrichtigungen sind deaktiviert</string>
@@ -252,9 +252,11 @@
<string name="pref_force_encryption_summary">Nachrichten immer verschlüsseln (außer für Konferenzen)</string>
<string name="pref_dont_save_encrypted">Verschlüsselte Nachrichten nicht speichern</string>
<string name="pref_dont_save_encrypted_summary">Achtung: Kann zu Nachrichtenverlust führen</string>
+ <string name="pref_enable_legacy_ssl">Alte SSL-Version aktivieren</string>
+ <string name="pref_enable_legacy_ssl_summary">Aktiviert SSLv3-Unterstützung für alte Server. Achtung: SSLv3 ist unsicher.</string>
<string name="pref_expert_options">Einstellungen für Experten</string>
<string name="pref_expert_options_summary">Hier bitte vorsichtig sein</string>
- <string name="pref_use_larger_font">Schriftgröße erhöhen</string>
+ <string name="pref_use_larger_font">Schrift vergrößern</string>
<string name="pref_use_larger_font_summary">Überall in der App eine größere Schrift verwenden</string>
<string name="pref_use_send_button_to_indicate_status">Absende-Knopf zeigt Online-Status an</string>
<string name="pref_use_send_button_to_indicate_status_summary">Absende-Knopf einfärben, um den Online-Status des Kontakts zu signalisieren</string>
@@ -265,5 +267,19 @@
<string name="conference_banned">Du wurdest aus dem Konferenzraum verbannt</string>
<string name="conference_members_only">Der Konferenzraum ist nur für Mitglieder</string>
<string name="conference_kicked">Du wurdest aus dem Konferenzraum geworfen</string>
+ <string name="using_account">Verwende Konto %s</string>
+ <string name="checking_image">Prüfe Bild auf HTTP-Host</string>
+ <string name="image_file_deleted">Bilddatei wurde gelöscht</string>
+ <string name="not_connected_try_again">Nicht verbunden, bitte später versuchen</string>
+ <string name="check_image_filesize">Bildgröße prüfen</string>
+ <string name="message_options">Nachrichtenoptionen</string>
+ <string name="copy_text">Text kopieren</string>
+ <string name="share_image">Bild teilen</string>
+ <string name="copy_original_url">Original-URL kopieren</string>
+ <string name="send_again">Erneut senden</string>
+ <string name="image_url">Bild-URL</string>
+ <string name="message_text">Nachrichtentext</string>
+ <string name="url_copied_to_clipboard">URL in Zwischenablage kopiert</string>
+ <string name="message_copied_to_clipboard">Nachricht in Zwischenablage kopiert</string>
</resources>
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