diff options
Diffstat (limited to 'src/main/java/eu/siacs/conversations')
48 files changed, 1609 insertions, 517 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 81a68008..fa9345a2 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -2,6 +2,8 @@ package eu.siacs.conversations; import android.graphics.Bitmap; +import eu.siacs.conversations.xmpp.chatstate.ChatState; + public final class Config { public static final String LOGTAG = "conversations"; @@ -23,21 +25,26 @@ public final class Config { public static final int PAGE_SIZE = 50; public static final int MAX_NUM_PAGES = 3; - public static final int PROGRESS_UI_UPDATE_INTERVAL = 750; + public static final int PROGRESS_UI_UPDATE_INTERVAL = 750; + public static final int REFRESH_UI_INTERVAL = 500; public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb + public static final boolean DISABLE_STRING_PREP = false; // setting to true might increase startup performance public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2; public static final int MAM_MAX_MESSAGES = 500; + public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE; + public static final int TYPING_TIMEOUT = 8; + public static final String ENABLED_CIPHERS[] = { "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA384", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_AES_128_SHA", - "TLS_ECDHE_RSA_AES_256_SHA", + "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", + "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_DHE_RSA_WITH_AES_128_GCM_SHA384", @@ -59,6 +66,15 @@ public final class Config { "TLS_RSA_WITH_AES_256_CBC_SHA", }; + public static final String WEAK_CIPHER_PATTERNS[] = { + "_NULL_", + "_EXPORT_", + "_anon_", + "_RC4_", + "_DES_", + "_MD5", + }; + private Config() { } diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java index d5c45465..20427d7b 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java @@ -21,6 +21,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -182,14 +183,39 @@ public class OtrEngine extends OtrCryptoEngineImpl implements OtrEngineHost { packet.addChild("private", "urn:xmpp:carbons:2"); packet.addChild("no-copy", "urn:xmpp:hints"); packet.addChild("no-store", "urn:xmpp:hints"); + + try { + Jid jid = Jid.fromSessionID(session); + Conversation conversation = mXmppConnectionService.find(account,jid); + if (conversation != null && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (mXmppConnectionService.sendChatStates()) { + packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); + } + } + } catch (final InvalidJidException ignored) { + + } + packet.setType(MessagePacket.TYPE_CHAT); account.getXmppConnection().sendMessagePacket(packet); } @Override - public void messageFromAnotherInstanceReceived(SessionID id) { - Log.d(Config.LOGTAG, - "unreadable message received from " + id.getAccountID()); + public void messageFromAnotherInstanceReceived(SessionID session) { + try { + Jid jid = Jid.fromSessionID(session); + Conversation conversation = mXmppConnectionService.find(account, jid); + String id = conversation == null ? null : conversation.getLastReceivedOtrMessageId(); + if (id != null) { + MessagePacket packet = mXmppConnectionService.getMessageGenerator().generateOtrError(jid,id); + packet.setFrom(account.getJid()); + mXmppConnectionService.sendMessagePacket(account,packet); + Log.d(Config.LOGTAG,packet.toString()); + Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": unreadable OTR message in "+conversation.getName()); + } + } catch (InvalidJidException e) { + return; + } } @Override diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index b0cde62c..2bc2c954 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -148,7 +148,7 @@ public class Account extends AbstractEntity { try { this.keys = new JSONObject(keys); } catch (final JSONException ignored) { - + this.keys = new JSONObject(); } this.avatar = avatar; } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 698e0322..cef03ebe 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -80,7 +80,7 @@ public class Contact implements ListItem, Blockable { cursor.getLong(cursor.getColumnIndex(LAST_TIME))); final Jid jid; try { - jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID))); + jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true); } catch (final InvalidJidException e) { // TODO: Borked DB... handle this somehow? return null; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 036acf63..bfee5007 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -2,10 +2,8 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; -import android.os.SystemClock; import net.java.otr4j.OtrException; -import net.java.otr4j.crypto.OtrCryptoEngineImpl; import net.java.otr4j.crypto.OtrCryptoException; import net.java.otr4j.session.SessionID; import net.java.otr4j.session.SessionImpl; @@ -21,6 +19,7 @@ import java.util.Comparator; import java.util.List; import eu.siacs.conversations.Config; +import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -77,6 +76,9 @@ public class Conversation extends AbstractEntity implements Blockable { private Bookmark bookmark; private boolean messagesLeftOnServer = true; + private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE; + private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE; + private String mLastReceivedOtrMessageId = null; public boolean hasMessagesLeftOnServer() { return messagesLeftOnServer; @@ -138,6 +140,34 @@ public class Conversation extends AbstractEntity implements Blockable { } } + public boolean setIncomingChatState(ChatState state) { + if (this.mIncomingChatState == state) { + return false; + } + this.mIncomingChatState = state; + return true; + } + + public ChatState getIncomingChatState() { + return this.mIncomingChatState; + } + + public boolean setOutgoingChatState(ChatState state) { + if (mode == MODE_MULTI) { + return false; + } + if (this.mOutgoingChatState != state) { + this.mOutgoingChatState = state; + return true; + } else { + return false; + } + } + + public ChatState getOutgoingChatState() { + return this.mOutgoingChatState; + } + public void trim() { synchronized (this.messages) { final int size = messages.size(); @@ -205,6 +235,14 @@ public class Conversation extends AbstractEntity implements Blockable { return getContact().getBlockedJid(); } + public String getLastReceivedOtrMessageId() { + return this.mLastReceivedOtrMessageId; + } + + public void setLastReceivedOtrMessageId(String id) { + this.mLastReceivedOtrMessageId = id; + } + public interface OnMessageFound { public void onMessageFound(final Message message); @@ -340,7 +378,7 @@ public class Conversation extends AbstractEntity implements Blockable { public static Conversation fromCursor(Cursor cursor) { Jid jid; try { - jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID))); + jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true); } catch (final InvalidJidException e) { // Borked DB.. jid = null; @@ -626,8 +664,7 @@ public class Conversation extends AbstractEntity implements Blockable { } public boolean isMuted() { - return SystemClock.elapsedRealtime() < this.getLongAttribute( - ATTRIBUTE_MUTED_TILL, 0); + return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0); } public boolean setAttribute(String key, String value) { @@ -706,6 +743,19 @@ public class Conversation extends AbstractEntity implements Blockable { } } + public int unreadCount() { + synchronized (this.messages) { + int count = 0; + for(int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).isRead()) { + return count; + } + ++count; + } + return count; + } + } + public class Smp { public static final int STATUS_NONE = 0; public static final int STATUS_CONTACT_REQUESTED = 1; diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 8112f5de..8015eead 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -8,6 +8,7 @@ import java.net.URL; import java.util.Arrays; import eu.siacs.conversations.Config; +import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -49,6 +50,7 @@ public class Message extends AbstractEntity { public static final String RELATIVE_FILE_PATH = "relativeFilePath"; public static final String ME_COMMAND = "/me "; + public boolean markable = false; protected String conversationUuid; protected Jid counterpart; @@ -115,7 +117,7 @@ public class Message extends AbstractEntity { try { String value = cursor.getString(cursor.getColumnIndex(COUNTERPART)); if (value != null) { - jid = Jid.fromString(value); + jid = Jid.fromString(value, true); } else { jid = null; } @@ -126,7 +128,7 @@ public class Message extends AbstractEntity { try { String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART)); if (value != null) { - trueCounterpart = Jid.fromString(value); + trueCounterpart = Jid.fromString(value, true); } else { trueCounterpart = null; } @@ -147,10 +149,11 @@ public class Message extends AbstractEntity { cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); } - public static Message createStatusMessage(Conversation conversation) { + public static Message createStatusMessage(Conversation conversation, String body) { Message message = new Message(); message.setType(Message.TYPE_STATUS); message.setConversation(conversation); + message.setBody(body); return message; } @@ -361,11 +364,14 @@ public class Message extends AbstractEntity { message.getDownloadable() == null && message.getEncryption() != Message.ENCRYPTION_PGP && this.getType() == message.getType() && - this.getStatus() == message.getStatus() && + //this.getStatus() == message.getStatus() && + isStatusMergeable(this.getStatus(),message.getStatus()) && this.getEncryption() == message.getEncryption() && this.getCounterpart() != null && this.getCounterpart().equals(message.getCounterpart()) && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && + !GeoHelper.isGeoUri(message.getBody()) && + !GeoHelper.isGeoUri(this.body) && !message.bodyContainsDownloadable() && !this.bodyContainsDownloadable() && !message.getBody().startsWith(ME_COMMAND) && @@ -373,12 +379,23 @@ public class Message extends AbstractEntity { ); } + private static boolean isStatusMergeable(int a, int b) { + return a == b || ( + ( a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND) + || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND) + || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED) + || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND) + || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED) + ); + } + public String getMergedBody() { final Message next = this.next(); if (this.mergeable(next)) { - return getBody() + '\n' + next.getMergedBody(); + return getBody().trim() + '\n' + next.getMergedBody(); } - return getBody(); + return getBody().trim(); } public boolean hasMeCommand() { @@ -386,6 +403,10 @@ public class Message extends AbstractEntity { } public int getMergedStatus() { + final Message next = this.next(); + if (this.mergeable(next)) { + return next.getStatus(); + } return getStatus(); } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 27821c65..addee8db 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -150,6 +150,21 @@ public class MucOptions { } } + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (!(other instanceof User)) { + return false; + } else { + User o = (User) other; + return name != null && name.equals(o.name) + && jid != null && jid.equals(o.jid) + && affiliation == o.affiliation + && role == o.role; + } + } + public Affiliation getAffiliation() { return this.affiliation; } @@ -260,7 +275,7 @@ public class MucOptions { User user = new User(); if (x != null) { Element item = x.findChild("item"); - if (item != null) { + if (item != null && name != null) { user.setName(name); user.setAffiliation(item.getAttribute("affiliation")); user.setRole(item.getAttribute("role")); diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 526e5b19..79626511 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -13,6 +13,7 @@ import java.util.Locale; import java.util.TimeZone; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.PhoneHelper; public abstract class AbstractGenerator { private final String[] FEATURES = { @@ -25,15 +26,18 @@ public abstract class AbstractGenerator { "http://jabber.org/protocol/caps", "http://jabber.org/protocol/disco#info", "urn:xmpp:avatar:metadata+notify", - "urn:xmpp:ping"}; + "urn:xmpp:ping", + "jabber:iq:version", + "http://jabber.org/protocol/chatstates"}; private final String[] MESSAGE_CONFIRMATION_FEATURES = { "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" }; - public final String IDENTITY_NAME = "Conversations 1.0"; + private String mVersion = null; + public final String IDENTITY_NAME = "Conversations"; public final String IDENTITY_TYPE = "phone"; - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); protected XmppConnectionService mXmppConnectionService; @@ -41,10 +45,21 @@ public abstract class AbstractGenerator { this.mXmppConnectionService = service; } + protected String getIdentityVersion() { + if (mVersion == null) { + this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); + } + return this.mVersion; + } + + protected String getIdentityName() { + return IDENTITY_NAME + " " + getIdentityVersion(); + } + public String getCapHash() { StringBuilder s = new StringBuilder(); - s.append("client/" + IDENTITY_TYPE + "//" + IDENTITY_NAME + "<"); - MessageDigest md = null; + s.append("client/" + IDENTITY_TYPE + "//" + getIdentityName() + "<"); + MessageDigest md; try { md = MessageDigest.getInstance("SHA-1"); } catch (NoSuchAlgorithmException e) { diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 161e6f89..6bc629b5 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -8,6 +8,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.PhoneHelper; import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.forms.Data; @@ -30,14 +31,22 @@ public class IqGenerator extends AbstractGenerator { query.setAttribute("node", request.query().getAttribute("node")); final Element identity = query.addChild("identity"); identity.setAttribute("category", "client"); - identity.setAttribute("type", this.IDENTITY_TYPE); - identity.setAttribute("name", IDENTITY_NAME); + identity.setAttribute("type", IDENTITY_TYPE); + identity.setAttribute("name", getIdentityName()); for (final String feature : getFeatures()) { query.addChild("feature").setAttribute("var", feature); } return packet; } + public IqPacket versionResponse(final IqPacket request) { + final IqPacket packet = request.generateResponse(IqPacket.TYPE.RESULT); + Element query = packet.query("jabber:iq:version"); + query.addChild("name").setContent(IDENTITY_NAME); + query.addChild("version").setContent(getIdentityVersion()); + return packet; + } + protected IqPacket publish(final String node, final Element item) { final IqPacket packet = new IqPacket(IqPacket.TYPE.SET); final Element pubsub = packet.addChild("pubsub", diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 2ee636b5..8f6a90b9 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -12,6 +12,7 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -34,6 +35,9 @@ public class MessageGenerator extends AbstractGenerator { } else if (message.getType() == Message.TYPE_PRIVATE) { packet.setTo(message.getCounterpart()); packet.setType(MessagePacket.TYPE_CHAT); + if (this.mXmppConnectionService.indicateReceived()) { + packet.addChild("request", "urn:xmpp:receipts"); + } } else { packet.setTo(message.getCounterpart().toBareJid()); packet.setType(MessagePacket.TYPE_GROUPCHAT); @@ -102,21 +106,12 @@ public class MessageGenerator extends AbstractGenerator { return packet; } - public MessagePacket generateNotAcceptable(MessagePacket origin) { - MessagePacket packet = generateError(origin); - Element error = packet.addChild("error"); - error.setAttribute("type", "modify"); - error.setAttribute("code", "406"); - error.addChild("not-acceptable"); - return packet; - } - - private MessagePacket generateError(MessagePacket origin) { + public MessagePacket generateChatState(Conversation conversation) { + final Account account = conversation.getAccount(); MessagePacket packet = new MessagePacket(); - packet.setId(origin.getId()); - packet.setTo(origin.getFrom()); - packet.setBody(origin.getBody()); - packet.setType(MessagePacket.TYPE_ERROR); + packet.setTo(conversation.getJid().toBareJid()); + packet.setFrom(account.getJid()); + packet.addChild(ChatState.toElement(conversation.getOutgoingChatState())); return packet; } @@ -176,4 +171,17 @@ public class MessageGenerator extends AbstractGenerator { received.setAttribute("id", originalMessage.getId()); return receivedPacket; } + + public MessagePacket generateOtrError(Jid to, String id) { + MessagePacket packet = new MessagePacket(); + packet.setType(MessagePacket.TYPE_ERROR); + packet.setAttribute("id",id); + packet.setTo(to); + Element error = packet.addChild("error"); + error.setAttribute("code","406"); + error.setAttribute("type","modify"); + error.addChild("not-acceptable","urn:ietf:params:xml:ns:xmpp-stanzas"); + error.addChild("text").setContent("unreadable OTR message received"); + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnection.java b/src/main/java/eu/siacs/conversations/http/HttpConnection.java index 4bff5251..e7d30919 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnection.java @@ -148,7 +148,7 @@ public class HttpConnection implements Downloadable { mXmppConnectionService.getRNG()); final SSLSocketFactory sf = sc.getSocketFactory(); - final String[] cipherSuites = CryptoHelper.getSupportedCipherSuites( + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( sf.getSupportedCipherSuites()); if (cipherSuites.length > 0) { sc.getDefaultSSLParameters().setCipherSuites(cipherSuites); diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index 9b3e239c..977062e2 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -64,6 +64,11 @@ public abstract class AbstractParser { protected void updateLastseen(final Element packet, final Account account, final boolean presenceOverwrite) { final Jid from = packet.getAttributeAsJid("from"); + updateLastseen(packet, account, from, presenceOverwrite); + } + + protected void updateLastseen(final Element packet, final Account account, final Jid from, + final boolean presenceOverwrite) { final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart(); final Contact contact = account.getRoster().getContact(from); final long timestamp = getTimestamp(packet); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 6430c296..6039d395 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -134,9 +134,11 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { mXmppConnectionService.getJingleConnectionManager() .deliverIbbPacket(account, packet); } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { - final IqPacket response = mXmppConnectionService.getIqGenerator() - .discoResponse(packet); - account.getXmppConnection().sendIqPacket(response, null); + final IqPacket response = mXmppConnectionService.getIqGenerator().discoResponse(packet); + mXmppConnectionService.sendIqPacket(account, response, null); + } else if (packet.hasChild("query","jabber:iq:version")) { + final IqPacket response = mXmppConnectionService.getIqGenerator().versionResponse(packet); + mXmppConnectionService.sendIqPacket(account,response,null); } else if (packet.hasChild("ping", "urn:xmpp:ping")) { final IqPacket response = packet.generateResponse(IqPacket.TYPE.RESULT); mXmppConnectionService.sendIqPacket(account, response, null); diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 49efb004..8ae9b642 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -14,6 +14,7 @@ import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -24,13 +25,27 @@ public class MessageParser extends AbstractParser implements super(service); } + private boolean extractChatState(Conversation conversation, final Element element) { + ChatState state = ChatState.parse(element); + if (state != null && conversation != null) { + final Account account = conversation.getAccount(); + Jid from = element.getAttributeAsJid("from"); + if (from != null && from.toBareJid().equals(account.getJid().toBareJid())) { + conversation.setOutgoingChatState(state); + return false; + } else { + return conversation.setIncomingChatState(state); + } + } + return false; + } + private Message parseChat(MessagePacket packet, Account account) { - final Jid jid = packet.getFrom(); + final Jid jid = packet.getFrom(); if (jid == null) { return null; } Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid.toBareJid(), false); - updateLastseen(packet, account, true); String pgpBody = getPgpBody(packet); Message finishedMessage; if (pgpBody != null) { @@ -45,16 +60,22 @@ public class MessageParser extends AbstractParser implements finishedMessage.markable = isMarkable(packet); if (conversation.getMode() == Conversation.MODE_MULTI && !jid.isBareJid()) { + final Jid trueCounterpart = conversation.getMucOptions() + .getTrueCounterpart(jid.getResourcepart()); + if (trueCounterpart != null) { + updateLastseen(packet, account, trueCounterpart, false); + } finishedMessage.setType(Message.TYPE_PRIVATE); - finishedMessage.setTrueCounterpart(conversation.getMucOptions() - .getTrueCounterpart(jid.getResourcepart())); + finishedMessage.setTrueCounterpart(trueCounterpart); if (conversation.hasDuplicateMessage(finishedMessage)) { return null; } - + } else { + updateLastseen(packet, account, true); } finishedMessage.setCounterpart(jid); finishedMessage.setTime(getTimestamp(packet)); + extractChatState(conversation,packet); return finishedMessage; } @@ -69,10 +90,11 @@ public class MessageParser extends AbstractParser implements .findOrCreateConversation(account, from.toBareJid(), false); String presence; if (from.isBareJid()) { - presence = ""; + presence = ""; } else { presence = from.getResourcepart(); } + extractChatState(conversation, packet); updateLastseen(packet, account, true); String body = packet.getBody(); if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { @@ -97,6 +119,7 @@ public class MessageParser extends AbstractParser implements } } try { + conversation.setLastReceivedOtrMessageId(packet.getId()); Session otrSession = conversation.getOtrSession(); SessionStatus before = otrSession.getSessionStatus(); body = otrSession.transformReceiving(body); @@ -123,6 +146,7 @@ public class MessageParser extends AbstractParser implements finishedMessage.setRemoteMsgId(packet.getId()); finishedMessage.markable = isMarkable(packet); finishedMessage.setCounterpart(from); + conversation.setLastReceivedOtrMessageId(null); return finishedMessage; } catch (Exception e) { conversation.resetOtrSession(); @@ -132,7 +156,7 @@ public class MessageParser extends AbstractParser implements private Message parseGroupchat(MessagePacket packet, Account account) { int status; - final Jid from = packet.getFrom(); + final Jid from = packet.getFrom(); if (from == null) { return null; } @@ -142,6 +166,10 @@ public class MessageParser extends AbstractParser implements } Conversation conversation = mXmppConnectionService .findOrCreateConversation(account, from.toBareJid(), true); + final Jid trueCounterpart = conversation.getMucOptions().getTrueCounterpart(from.getResourcepart()); + if (trueCounterpart != null) { + updateLastseen(packet, account, trueCounterpart, false); + } if (packet.hasChild("subject")) { conversation.setHasMessagesLeftOnServer(true); conversation.getMucOptions().setSubject(packet.findChild("subject").getContent()); @@ -275,6 +303,7 @@ public class MessageParser extends AbstractParser implements finishedMessage = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } + extractChatState(conversation,message); finishedMessage.setTime(getTimestamp(message)); finishedMessage.setRemoteMsgId(message.getAttribute("id")); finishedMessage.markable = isMarkable(message); @@ -362,6 +391,9 @@ public class MessageParser extends AbstractParser implements private void parseNonMessage(Element packet, Account account) { final Jid from = packet.getAttributeAsJid("from"); + if (extractChatState(from == null ? null : mXmppConnectionService.find(account,from), packet)) { + mXmppConnectionService.updateConversationUi(); + } Element invite = extractInvite(packet); if (invite != null) { Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, from, true); @@ -377,14 +409,19 @@ public class MessageParser extends AbstractParser implements Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event"); parseEvent(event, from, account); - } else if (from != null - && packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) { + } else if (from != null && packet.hasChild("displayed", "urn:xmpp:chat-markers:0")) { String id = packet .findChild("displayed", "urn:xmpp:chat-markers:0") .getAttribute("id"); updateLastseen(packet, account, true); - mXmppConnectionService.markMessage(account, from.toBareJid(), - id, Message.STATUS_SEND_DISPLAYED); + final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), id, Message.STATUS_SEND_DISPLAYED); + Message message = displayedMessage == null ? null :displayedMessage.prev(); + while (message != null + && message.getStatus() == Message.STATUS_SEND_RECEIVED + && message.getTimeSent() < displayedMessage.getTimeSent()) { + mXmppConnectionService.markMessage(message, Message.STATUS_SEND_DISPLAYED); + message = message.prev(); + } } else if (from != null && packet.hasChild("received", "urn:xmpp:chat-markers:0")) { String id = packet.findChild("received", "urn:xmpp:chat-markers:0") diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index accb56ea..7505b091 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -1,5 +1,7 @@ package eu.siacs.conversations.parser; +import java.util.ArrayList; + import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -27,8 +29,12 @@ public class PresenceParser extends AbstractParser implements final MucOptions mucOptions = conversation.getMucOptions(); boolean before = mucOptions.online(); int count = mucOptions.getUsers().size(); + final ArrayList<MucOptions.User> tileUserBefore = new ArrayList<>(mucOptions.getUsers().subList(0,Math.min(mucOptions.getUsers().size(),5))); mucOptions.processPacket(packet, mPgpEngine); - mXmppConnectionService.getAvatarService().clear(conversation); + final ArrayList<MucOptions.User> tileUserAfter = new ArrayList<>(mucOptions.getUsers().subList(0,Math.min(mucOptions.getUsers().size(),5))); + if (!tileUserAfter.equals(tileUserBefore)) { + mXmppConnectionService.getAvatarService().clear(conversation); + } if (before != mucOptions.online() || (mucOptions.online() && count != mucOptions.getUsers().size())) { mXmppConnectionService.updateConversationUi(); } else if (mucOptions.online()) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 3ae3356d..28e1c47e 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -278,6 +278,8 @@ public class DatabaseBackend extends SQLiteOpenHelper { return (count > 0); } catch (SQLiteCantOpenDatabaseException e) { return true; // better safe than sorry + } catch (RuntimeException e) { + return true; // better safe than sorry } } diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 62987aaa..c499d499 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -318,39 +318,41 @@ public class FileBackend { } public boolean save(Avatar avatar) { + File file; if (isAvatarCached(avatar)) { - return true; - } - String filename = getAvatarPath(avatar.getFilename()); - File file = new File(filename + ".tmp"); - file.getParentFile().mkdirs(); - try { - file.createNewFile(); - FileOutputStream mFileOutputStream = new FileOutputStream(file); - MessageDigest digest = MessageDigest.getInstance("SHA-1"); - digest.reset(); - DigestOutputStream mDigestOutputStream = new DigestOutputStream( - mFileOutputStream, digest); - mDigestOutputStream.write(avatar.getImageAsBytes()); - mDigestOutputStream.flush(); - mDigestOutputStream.close(); - avatar.size = file.length(); - String sha1sum = CryptoHelper.bytesToHex(digest.digest()); - if (sha1sum.equals(avatar.sha1sum)) { - file.renameTo(new File(filename)); - return true; - } else { - Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); - file.delete(); + file = new File(getAvatarPath(avatar.getFilename())); + } else { + String filename = getAvatarPath(avatar.getFilename()); + file = new File(filename + ".tmp"); + file.getParentFile().mkdirs(); + try { + file.createNewFile(); + FileOutputStream mFileOutputStream = new FileOutputStream(file); + MessageDigest digest = MessageDigest.getInstance("SHA-1"); + digest.reset(); + DigestOutputStream mDigestOutputStream = new DigestOutputStream( + mFileOutputStream, digest); + mDigestOutputStream.write(avatar.getImageAsBytes()); + mDigestOutputStream.flush(); + mDigestOutputStream.close(); + String sha1sum = CryptoHelper.bytesToHex(digest.digest()); + if (sha1sum.equals(avatar.sha1sum)) { + file.renameTo(new File(filename)); + } else { + Log.d(Config.LOGTAG, "sha1sum mismatch for " + avatar.owner); + file.delete(); + return false; + } + } catch (FileNotFoundException e) { + return false; + } catch (IOException e) { + return false; + } catch (NoSuchAlgorithmException e) { return false; } - } catch (FileNotFoundException e) { - return false; - } catch (IOException e) { - return false; - } catch (NoSuchAlgorithmException e) { - return false; } + avatar.size = file.length(); + return true; } public String getAvatarPath(String avatar) { diff --git a/src/main/java/eu/siacs/conversations/services/AvatarService.java b/src/main/java/eu/siacs/conversations/services/AvatarService.java index f28dc24e..7412eb93 100644 --- a/src/main/java/eu/siacs/conversations/services/AvatarService.java +++ b/src/main/java/eu/siacs/conversations/services/AvatarService.java @@ -38,10 +38,10 @@ public class AvatarService { this.mXmppConnectionService = service; } - public Bitmap get(final Contact contact, final int size) { + private Bitmap get(final Contact contact, final int size, boolean cachedOnly) { final String KEY = key(contact, size); Bitmap avatar = this.mXmppConnectionService.getBitmapCache().get(KEY); - if (avatar != null) { + if (avatar != null || cachedOnly) { return avatar; } if (contact.getProfilePhoto() != null) { @@ -51,7 +51,7 @@ public class AvatarService { avatar = mXmppConnectionService.getFileBackend().getAvatar(contact.getAvatar(), size); } if (avatar == null) { - avatar = get(contact.getDisplayName(), size); + avatar = get(contact.getDisplayName(), size, cachedOnly); } this.mXmppConnectionService.getBitmapCache().put(KEY, avatar); return avatar; @@ -77,25 +77,33 @@ public class AvatarService { } public Bitmap get(ListItem item, int size) { + return get(item,size,false); + } + + public Bitmap get(ListItem item, int size, boolean cachedOnly) { if (item instanceof Contact) { - return get((Contact) item, size); + return get((Contact) item, size,cachedOnly); } else if (item instanceof Bookmark) { Bookmark bookmark = (Bookmark) item; if (bookmark.getConversation() != null) { - return get(bookmark.getConversation(), size); + return get(bookmark.getConversation(), size, cachedOnly); } else { - return get(bookmark.getDisplayName(), size); + return get(bookmark.getDisplayName(), size, cachedOnly); } } else { - return get(item.getDisplayName(), size); + return get(item.getDisplayName(), size, cachedOnly); } } public Bitmap get(Conversation conversation, int size) { + return get(conversation,size,false); + } + + public Bitmap get(Conversation conversation, int size, boolean cachedOnly) { if (conversation.getMode() == Conversation.MODE_SINGLE) { - return get(conversation.getContact(), size); + return get(conversation.getContact(), size, cachedOnly); } else { - return get(conversation.getMucOptions(), size); + return get(conversation.getMucOptions(), size, cachedOnly); } } @@ -107,10 +115,10 @@ public class AvatarService { } } - public Bitmap get(MucOptions mucOptions, int size) { + private Bitmap get(MucOptions mucOptions, int size, boolean cachedOnly) { final String KEY = key(mucOptions, size); Bitmap bitmap = this.mXmppConnectionService.getBitmapCache().get(KEY); - if (bitmap != null) { + if (bitmap != null || cachedOnly) { return bitmap; } final List<MucOptions.User> users = new ArrayList<>(mucOptions.getUsers()); @@ -179,7 +187,7 @@ public class AvatarService { avatar = mXmppConnectionService.getFileBackend().getAvatar( account.getAvatar(), size); if (avatar == null) { - avatar = get(account.getJid().toBareJid().toString(), size); + avatar = get(account.getJid().toBareJid().toString(), size,false); } mXmppConnectionService.getBitmapCache().put(KEY, avatar); return avatar; @@ -204,10 +212,14 @@ public class AvatarService { + String.valueOf(size); } - public Bitmap get(final String name, final int size) { + public Bitmap get(String name, int size) { + return get(name,size,false); + } + + public Bitmap get(final String name, final int size, boolean cachedOnly) { final String KEY = key(name, size); Bitmap bitmap = mXmppConnectionService.getBitmapCache().get(KEY); - if (bitmap != null) { + if (bitmap != null || cachedOnly) { return bitmap; } bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888); diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 2ea0904f..7269a559 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -40,7 +40,9 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ManageAccountActivity; import eu.siacs.conversations.ui.TimePreference; +import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.XmppConnection; public class NotificationService { @@ -210,9 +212,10 @@ public class NotificationService { mBuilder.setCategory(Notification.CATEGORY_MESSAGE); } setNotificationColor(mBuilder); + mBuilder.setDefaults(0); mBuilder.setSmallIcon(R.drawable.ic_notification); mBuilder.setDeleteIntent(createDeleteIntent()); - mBuilder.setLights(0xffffffff, 2000, 4000); + mBuilder.setLights(0xff00FF00, 2000, 3000); final Notification notification = mBuilder.build(); notificationManager.notify(NOTIFICATION_ID, notification); } @@ -277,6 +280,11 @@ public class NotificationService { createDownloadIntent(message) ); } + if ((message = getFirstLocationMessage(messages)) != null) { + mBuilder.addAction(R.drawable.ic_room_white_24dp, + mXmppConnectionService.getString(R.string.show_location), + createShowLocationIntent(message)); + } mBuilder.setContentIntent(createContentIntent(conversation)); } return mBuilder; @@ -340,6 +348,15 @@ public class NotificationService { return null; } + private Message getFirstLocationMessage(final Iterable<Message> messages) { + for(final Message message : messages) { + if (GeoHelper.isGeoUri(message.getBody())) { + return message; + } + } + return null; + } + private CharSequence getMergedBodies(final ArrayList<Message> messages) { final StringBuilder text = new StringBuilder(); for (int i = 0; i < messages.size(); ++i) { @@ -351,6 +368,16 @@ public class NotificationService { return text.toString(); } + private PendingIntent createShowLocationIntent(final Message message) { + Iterable<Intent> intents = GeoHelper.createGeoIntentsFromMessage(message); + for(Intent intent : intents) { + if (intent.resolveActivity(mXmppConnectionService.getPackageManager()) != null) { + return PendingIntent.getActivity(mXmppConnectionService,18,intent,PendingIntent.FLAG_UPDATE_CURRENT); + } + } + return createOpenConversationsIntent(); + } + private PendingIntent createContentIntent(final String conversationUuid, final String downloadMessageUuid) { final TaskStackBuilder stackBuilder = TaskStackBuilder .create(mXmppConnectionService); @@ -395,7 +422,20 @@ public class NotificationService { final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND); - return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); + return PendingIntent.getService(mXmppConnectionService, 34, intent, 0); + } + + private PendingIntent createTryAgainIntent() { + final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_TRY_AGAIN); + return PendingIntent.getService(mXmppConnectionService, 45, intent, 0); + } + + private PendingIntent createDisableAccountIntent(final Account account) { + final Intent intent = new Intent(mXmppConnectionService,XmppConnectionService.class); + intent.setAction(XmppConnectionService.ACTION_DISABLE_ACCOUNT); + intent.putExtra("account",account.getJid().toBareJid().toString()); + return PendingIntent.getService(mXmppConnectionService,0,intent,PendingIntent.FLAG_UPDATE_CURRENT); } private boolean wasHighlightedOrPrivate(final Message message) { @@ -423,9 +463,6 @@ public class NotificationService { } public void setIsInForeground(final boolean foreground) { - if (foreground != this.mIsInForeground) { - Log.d(Config.LOGTAG,"setIsInForeground("+Boolean.toString(foreground)+")"); - } this.mIsInForeground = foreground; } @@ -492,6 +529,14 @@ public class NotificationService { mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.problem_connecting_to_accounts)); mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_fix)); } + mBuilder.addAction(R.drawable.ic_autorenew_white_24dp, + mXmppConnectionService.getString(R.string.try_again), + createTryAgainIntent()); + if (errors.size() == 1) { + mBuilder.addAction(R.drawable.ic_block_white_24dp, + mXmppConnectionService.getString(R.string.disable_account), + createDisableAccountIntent(errors.get(0))); + } mBuilder.setOngoing(true); //mBuilder.setLights(0xffffffff, 2000, 4000); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index e34f9bd7..ca182867 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -16,6 +16,7 @@ import android.os.Binder; import android.os.Bundle; import android.os.FileObserver; import android.os.IBinder; +import android.os.Looper; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; @@ -85,6 +86,7 @@ import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.XmppConnection; +import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Field; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -102,6 +104,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification"; public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground"; private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static final String ACTION_TRY_AGAIN = "try_again"; + public static final String ACTION_DISABLE_ACCOUNT = "disable_account"; private ContentObserver contactObserver = new ContentObserver(null) { @Override public void onChange(boolean selfChange) { @@ -275,6 +279,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private LruCache<String, Bitmap> mBitmapCache; private Thread mPhoneContactMergerThread; + private boolean mRestoredFromDatabase = false; + public boolean areMessagesInitialized() { + return this.mRestoredFromDatabase; + } + public PgpEngine getPgpEngine() { if (pgpServiceConnection.isBound()) { if (this.mPgpEngine == null) { @@ -297,6 +306,24 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return this.mAvatarService; } + public void attachLocationToConversation(final Conversation conversation, + final Uri uri, + final UiCallback<Message> callback) { + int encryption = conversation.getNextEncryption(forceEncryption()); + if (encryption == Message.ENCRYPTION_PGP) { + encryption = Message.ENCRYPTION_DECRYPTED; + } + Message message = new Message(conversation,uri.toString(),encryption); + if (conversation.getNextCounterpart() != null) { + message.setCounterpart(conversation.getNextCounterpart()); + } + if (encryption == Message.ENCRYPTION_DECRYPTED) { + getPgpEngine().encrypt(message,callback); + } else { + callback.success(message); + } + } + public void attachFileToConversation(final Conversation conversation, final Uri uri, final UiCallback<Message> callback) { @@ -386,7 +413,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (action != null) { switch (action) { case ACTION_MERGE_PHONE_CONTACTS: - PhoneHelper.loadPhoneContacts(getApplicationContext(), new CopyOnWriteArrayList<Bundle>(), this); + if (mRestoredFromDatabase) { + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new CopyOnWriteArrayList<Bundle>(), + this); + } return START_STICKY; case Intent.ACTION_SHUTDOWN: logoutAndSave(); @@ -398,6 +429,28 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa getPreferences().edit().putBoolean("keep_foreground_service",false).commit(); toggleForegroundService(); break; + case ACTION_TRY_AGAIN: + for(Account account : accounts) { + if (account.hasErrorStatus()) { + final XmppConnection connection = account.getXmppConnection(); + if (connection != null) { + connection.resetAttemptCount(); + } + } + } + break; + case ACTION_DISABLE_ACCOUNT: + try { + String jid = intent.getStringExtra("account"); + Account account = jid == null ? null : findAccountByJid(Jid.fromString(jid)); + if (account != null) { + account.setOption(Account.OPTION_DISABLED,true); + updateAccount(account); + } + } catch (final InvalidJidException ignored) { + break; + } + break; } } this.wakeLock.acquire(); @@ -432,10 +485,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this.scheduleWakeUpCall((int) (msToNextPing / 1000), account.getUuid().hashCode()); } } else if (account.getStatus() == Account.State.OFFLINE) { - if (account.getXmppConnection() == null) { - account.setXmppConnection(this.createConnection(account)); - } - new Thread(account.getXmppConnection()).start(); + reconnectAccount(account,true); } else if (account.getStatus() == Account.State.CONNECTING) { long timeout = Config.CONNECT_TIMEOUT - ((SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000); if (timeout < 0) { @@ -499,10 +549,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (final Account account : this.accounts) { account.initOtrEngine(this); - this.databaseBackend.readRoster(account.getRoster()); } - initConversations(); - PhoneHelper.loadPhoneContacts(getApplicationContext(),new CopyOnWriteArrayList<Bundle>(), this); + restoreFromDatabase(); getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); this.fileObserver.startWatching(); @@ -574,6 +622,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return connection; } + public void sendChatState(Conversation conversation) { + if (sendChatStates()) { + MessagePacket packet = mMessageGenerator.generateChatState(conversation); + sendMessagePacket(conversation.getAccount(), packet); + } + } + public void sendMessage(final Message message) { final Account account = message.getConversation().getAccount(); account.deactivateGracePeriod(); @@ -674,6 +729,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } if ((send) && (packet != null)) { + if (conv.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (this.sendChatStates()) { + packet.addChild(ChatState.toElement(conv.getOutgoingChatState())); + } + } sendMessagePacket(account, packet); } updateConversationUi(); @@ -755,6 +815,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } else { markMessage(message, Message.STATUS_UNSEND); } + if (message.getConversation().setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + if (this.sendChatStates()) { + packet.addChild(ChatState.toElement(message.getConversation().getOutgoingChatState())); + } + } sendMessagePacket(account, packet); } } @@ -855,7 +920,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa mPhoneContactMergerThread.start(); } - private void initConversations() { + private void restoreFromDatabase() { synchronized (this.conversations) { final Map<String, Account> accountLookupTable = new Hashtable<>(); for (Account account : this.accounts) { @@ -865,9 +930,29 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (Conversation conversation : this.conversations) { Account account = accountLookupTable.get(conversation.getAccountUuid()); conversation.setAccount(account); - conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); - checkDeletedFiles(conversation); } + new Thread(new Runnable() { + @Override + public void run() { + Log.d(Config.LOGTAG,"restoring roster"); + for(Account account : accounts) { + databaseBackend.readRoster(account.getRoster()); + } + getBitmapCache().evictAll(); + Looper.prepare(); + PhoneHelper.loadPhoneContacts(getApplicationContext(), + new CopyOnWriteArrayList<Bundle>(), + XmppConnectionService.this); + Log.d(Config.LOGTAG,"restoring messages"); + for (Conversation conversation : conversations) { + conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); + checkDeletedFiles(conversation); + } + mRestoredFromDatabase = true; + Log.d(Config.LOGTAG,"restored all messages"); + updateConversationUi(); + } + }).start(); } } @@ -1069,7 +1154,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa account.initOtrEngine(this); databaseBackend.createAccount(account); this.accounts.add(account); - this.reconnectAccount(account, false); + this.reconnectAccountInBackground(account); updateAccountUi(); } @@ -1274,6 +1359,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } } + for(Conversation conversation : getConversations()) { + conversation.setIncomingChatState(ChatState.ACTIVE); + } this.mNotificationService.setIsInForeground(false); Log.d(Config.LOGTAG, "app switched into background"); } @@ -1902,24 +1990,29 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void reconnectAccount(final Account account, final boolean force) { - new Thread(new Runnable() { + synchronized (account) { + if (account.getXmppConnection() != null) { + disconnect(account, force); + } + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + if (account.getXmppConnection() == null) { + account.setXmppConnection(createConnection(account)); + } + Thread thread = new Thread(account.getXmppConnection()); + thread.start(); + scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode()); + } else { + account.getRoster().clearPresences(); + account.setXmppConnection(null); + } + } + } + public void reconnectAccountInBackground(final Account account) { + new Thread(new Runnable() { @Override public void run() { - if (account.getXmppConnection() != null) { - disconnect(account, force); - } - if (!account.isOptionSet(Account.OPTION_DISABLED)) { - if (account.getXmppConnection() == null) { - account.setXmppConnection(createConnection(account)); - } - Thread thread = new Thread(account.getXmppConnection()); - thread.start(); - scheduleWakeUpCall(Config.CONNECT_TIMEOUT, account.getUuid().hashCode()); - } else { - account.getRoster().clearPresences(); - account.setXmppConnection(null); - } + reconnectAccount(account,false); } }).start(); } @@ -1943,19 +2036,20 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } - public boolean markMessage(final Account account, final Jid recipient, final String uuid, - final int status) { + public Message markMessage(final Account account, final Jid recipient, final String uuid, final int status) { if (uuid == null) { - return false; - } else { - for (Conversation conversation : getConversations()) { - if (conversation.getJid().equals(recipient) - && conversation.getAccount().equals(account)) { - return markMessage(conversation, uuid, status); + return null; + } + for (Conversation conversation : getConversations()) { + if (conversation.getJid().toBareJid().equals(recipient) && conversation.getAccount() == account) { + final Message message = conversation.findSentMessageWithUuid(uuid); + if (message != null) { + markMessage(message, status); } + return message; } - return false; } + return null; } public boolean markMessage(Conversation conversation, String uuid, @@ -1997,6 +2091,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return getPreferences().getBoolean("confirm_messages", true); } + public boolean sendChatStates() { + return getPreferences().getBoolean("chat_states", false); + } + public boolean saveEncryptedMessages() { return !getPreferences().getBoolean("dont_save_encrypted", false); } @@ -2005,6 +2103,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return getPreferences().getBoolean("indicate_received", false); } + public int unreadCount() { + int count = 0; + for(Conversation conversation : getConversations()) { + count += conversation.unreadCount(); + } + return count; + } + public void updateConversationUi() { if (mOnConversationUpdate != null) { mOnConversationUpdate.onConversationUpdate(); diff --git a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java index 804b4e23..a57e1b89 100644 --- a/src/main/java/eu/siacs/conversations/ui/AboutPreference.java +++ b/src/main/java/eu/siacs/conversations/ui/AboutPreference.java @@ -6,6 +6,8 @@ import android.content.pm.PackageManager; import android.preference.Preference; import android.util.AttributeSet; +import eu.siacs.conversations.utils.PhoneHelper; + public class AboutPreference extends Preference { public AboutPreference(final Context context, final AttributeSet attrs, final int defStyle) { super(context, attrs, defStyle); @@ -25,17 +27,7 @@ public class AboutPreference extends Preference { } private void setSummary() { - if (getContext() != null && getContext().getPackageManager() != null) { - final String packageName = getContext().getPackageName(); - final String versionName; - try { - versionName = getContext().getPackageManager().getPackageInfo(packageName, 0).versionName; - setSummary("Conversations " + versionName); - } catch (final PackageManager.NameNotFoundException e) { - // Using try/catch as part of the logic is sort of like this: - // https://xkcd.com/292/ - } - } + setSummary("Conversations " + PhoneHelper.getVersionName(getContext())); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java index 70b353c6..c9e99ce5 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -3,20 +3,100 @@ package eu.siacs.conversations.ui; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.view.ActionMode; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView.MultiChoiceModeListener; import android.widget.AdapterView; +import android.widget.ListView; +import java.util.Set; +import java.util.HashSet; import java.util.Collections; +import java.util.List; +import java.util.ArrayList; +import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; public class ChooseContactActivity extends AbstractSearchableListItemActivity { + + private Set<Contact> selected; + private Set<String> filterContacts; + @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); + filterContacts = new HashSet<>(); + String[] contacts = getIntent().getStringArrayExtra("filter_contacts"); + if (contacts != null) { + Collections.addAll(filterContacts, contacts); + } + + if (getIntent().getBooleanExtra("multiple", false)) { + getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL); + getListView().setMultiChoiceModeListener(new MultiChoiceModeListener() { + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.select_multiple, menu); + selected = new HashSet<Contact>(); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + switch(item.getItemId()) { + case R.id.selection_submit: + final Intent request = getIntent(); + final Intent data = new Intent(); + data.putExtra("conversation", + request.getStringExtra("conversation")); + String[] selection = getSelectedContactJids(); + data.putExtra("contacts", selection); + data.putExtra("multiple", true); + setResult(RESULT_OK, data); + finish(); + return true; + } + return false; + } + + @Override + public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + Contact item = (Contact) getListItems().get(position); + if (checked) { + selected.add(item); + } else { + selected.remove(item); + } + int numSelected = selected.size(); + MenuItem selectButton = mode.getMenu().findItem(R.id.selection_submit); + String buttonText = getResources().getQuantityString(R.plurals.select_contact, + numSelected, numSelected); + selectButton.setTitle(buttonText); + } + }); + } + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override @@ -36,6 +116,7 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity { data.putExtra("account", account); data.putExtra("conversation", request.getStringExtra("conversation")); + data.putExtra("multiple", false); setResult(RESULT_OK, data); finish(); } @@ -48,7 +129,9 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity { for (final Account account : xmppConnectionService.getAccounts()) { if (account.getStatus() != Account.State.DISABLED) { for (final Contact contact : account.getRoster().getContacts()) { - if (contact.showInRoster() && contact.match(needle)) { + if (contact.showInRoster() && + !filterContacts.contains(contact.getJid().toBareJid().toString()) + && contact.match(needle)) { getListItems().add(contact); } } @@ -57,4 +140,13 @@ public class ChooseContactActivity extends AbstractSearchableListItemActivity { Collections.sort(getListItems()); getListItemAdapter().notifyDataSetChanged(); } + + private String[] getSelectedContactJids() { + List<String> result = new ArrayList<>(); + for (Contact contact : selected) { + result.add(contact.getJid().toString()); + } + return result.toArray(new String[result.size()]); + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 399d9fdf..e4bfd6ff 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -25,6 +25,8 @@ import android.widget.Toast; import org.openintents.openpgp.util.OpenPgpUtils; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; import eu.siacs.conversations.R; @@ -142,24 +144,17 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers @Override public void onConversationUpdate() { - runOnUiThread(new Runnable() { - - @Override - public void run() { - updateView(); - } - }); + refreshUi(); } @Override public void onMucRosterUpdate() { - runOnUiThread(new Runnable() { + refreshUi(); + } - @Override - public void run() { - updateView(); - } - }); + @Override + protected void refreshUiReal() { + updateView(); } @Override @@ -431,9 +426,16 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE); membersView.removeAllViews(); - for (final User user : mConversation.getMucOptions().getUsers()) { - View view = inflater.inflate(R.layout.contact, membersView, - false); + final ArrayList<User> users = new ArrayList<>(); + users.addAll(mConversation.getMucOptions().getUsers()); + Collections.sort(users,new Comparator<User>() { + @Override + public int compare(User lhs, User rhs) { + return lhs.getName().compareToIgnoreCase(rhs.getName()); + } + }); + for (final User user : users) { + View view = inflater.inflate(R.layout.contact, membersView,false); this.setListItemBackgroundOnView(view); view.setOnClickListener(new OnClickListener() { @Override diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index fda0c617..40a4587c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -139,26 +139,18 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd @Override public void onRosterUpdate() { - runOnUiThread(new Runnable() { - - @Override - public void run() { - invalidateOptionsMenu(); - populateView(); - } - }); + refreshUi(); } @Override public void onAccountUpdate() { - runOnUiThread(new Runnable() { + refreshUi(); + } - @Override - public void run() { - invalidateOptionsMenu(); - populateView(); - } - }); + @Override + protected void refreshUiReal() { + invalidateOptionsMenu(); + populateView(); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 0a55c6b5..82afda07 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -12,7 +12,6 @@ import android.content.IntentSender.SendIntentException; import android.net.Uri; import android.os.Build; import android.os.Bundle; -import android.os.SystemClock; import android.provider.MediaStore; import android.support.v4.widget.SlidingPaneLayout; import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; @@ -34,7 +33,6 @@ import java.util.ArrayList; import java.util.List; import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -64,6 +62,7 @@ public class ConversationActivity extends XmppActivity private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; private static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; + private static final int ATTACHMENT_CHOICE_LOCATION = 0x0305; private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; private static final String STATE_PANEL_OPEN = "state_panel_open"; private static final String STATE_PENDING_URI = "state_pending_uri"; @@ -72,6 +71,7 @@ public class ConversationActivity extends XmppActivity private boolean mPanelOpen = true; private Uri mPendingImageUri = null; private Uri mPendingFileUri = null; + private Uri mPendingGeoUri = null; private View mContentView; @@ -85,6 +85,7 @@ public class ConversationActivity extends XmppActivity private Toast prepareFileToast; private boolean mActivityPaused = false; + private boolean mRedirected = true; public Conversation getSelectedConversation() { return this.mSelectedConversation; @@ -313,7 +314,6 @@ public class ConversationActivity extends XmppActivity menuInviteContact.setVisible(getSelectedConversation().getMucOptions().canInvite()); } else { menuMucDetails.setVisible(false); - final Account account = this.getSelectedConversation().getAccount(); } if (this.getSelectedConversation().isMuted()) { menuMute.setVisible(false); @@ -325,50 +325,60 @@ public class ConversationActivity extends XmppActivity return true; } - private void selectPresenceToAttachFile(final int attachmentChoice) { - selectPresence(getSelectedConversation(), new OnPresenceSelected() { + private void selectPresenceToAttachFile(final int attachmentChoice, final int encryption) { + if (attachmentChoice == ATTACHMENT_CHOICE_LOCATION && encryption != Message.ENCRYPTION_OTR) { + getSelectedConversation().setNextCounterpart(null); + Intent intent = new Intent("eu.siacs.conversations.location.request"); + startActivityForResult(intent,attachmentChoice); + } else { + selectPresence(getSelectedConversation(), new OnPresenceSelected() { - @Override - public void onPresenceSelected() { - Intent intent = new Intent(); - boolean chooser = false; - switch (attachmentChoice) { - case ATTACHMENT_CHOICE_CHOOSE_IMAGE: - intent.setAction(Intent.ACTION_GET_CONTENT); - intent.setType("image/*"); - chooser = true; - break; - case ATTACHMENT_CHOICE_TAKE_PHOTO: - mPendingImageUri = xmppConnectionService.getFileBackend().getTakePhotoUri(); - intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); - intent.putExtra(MediaStore.EXTRA_OUTPUT,mPendingImageUri); - break; - case ATTACHMENT_CHOICE_CHOOSE_FILE: - chooser = true; - intent.setType("*/*"); - intent.addCategory(Intent.CATEGORY_OPENABLE); - intent.setAction(Intent.ACTION_GET_CONTENT); - break; - case ATTACHMENT_CHOICE_RECORD_VOICE: - intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION); - break; - } - if (intent.resolveActivity(getPackageManager()) != null) { - if (chooser) { - startActivityForResult( - Intent.createChooser(intent,getString(R.string.perform_action_with)), - attachmentChoice); - } else { - startActivityForResult(intent, attachmentChoice); + @Override + public void onPresenceSelected() { + Intent intent = new Intent(); + boolean chooser = false; + switch (attachmentChoice) { + case ATTACHMENT_CHOICE_CHOOSE_IMAGE: + intent.setAction(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + chooser = true; + break; + case ATTACHMENT_CHOICE_TAKE_PHOTO: + mPendingImageUri = xmppConnectionService.getFileBackend().getTakePhotoUri(); + intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT, mPendingImageUri); + break; + case ATTACHMENT_CHOICE_CHOOSE_FILE: + chooser = true; + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setAction(Intent.ACTION_GET_CONTENT); + break; + case ATTACHMENT_CHOICE_RECORD_VOICE: + intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + break; + case ATTACHMENT_CHOICE_LOCATION: + intent.setAction("eu.siacs.conversations.location.request"); + break; + } + if (intent.resolveActivity(getPackageManager()) != null) { + if (chooser) { + startActivityForResult( + Intent.createChooser(intent, getString(R.string.perform_action_with)), + attachmentChoice); + } else { + startActivityForResult(intent, attachmentChoice); + } } } - } - }); + }); + } } private void attachFile(final int attachmentChoice) { final Conversation conversation = getSelectedConversation(); - if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { + final int encryption = conversation.getNextEncryption(forceEncryption()); + if (encryption == Message.ENCRYPTION_PGP) { if (hasPgp()) { if (conversation.getContact().getPgpKeyId() != 0) { xmppConnectionService.getPgpEngine().hasKey( @@ -378,13 +388,12 @@ public class ConversationActivity extends XmppActivity @Override public void userInputRequried(PendingIntent pi, Contact contact) { - ConversationActivity.this.runIntent(pi, - attachmentChoice); + ConversationActivity.this.runIntent(pi,attachmentChoice); } @Override public void success(Contact contact) { - selectPresenceToAttachFile(attachmentChoice); + selectPresenceToAttachFile(attachmentChoice,encryption); } @Override @@ -406,7 +415,7 @@ public class ConversationActivity extends XmppActivity .setNextEncryption(Message.ENCRYPTION_NONE); xmppConnectionService.databaseBackend .updateConversation(conversation); - selectPresenceToAttachFile(attachmentChoice); + selectPresenceToAttachFile(attachmentChoice,Message.ENCRYPTION_NONE); } }); } @@ -414,11 +423,8 @@ public class ConversationActivity extends XmppActivity } else { showInstallPgpDialog(); } - } else if (getSelectedConversation().getNextEncryption( - forceEncryption()) == Message.ENCRYPTION_NONE) { - selectPresenceToAttachFile(attachmentChoice); } else { - selectPresenceToAttachFile(attachmentChoice); + selectPresenceToAttachFile(attachmentChoice,encryption); } } @@ -526,6 +532,9 @@ public class ConversationActivity extends XmppActivity if (new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null) { attachFilePopup.getMenu().findItem(R.id.attach_record_voice).setVisible(false); } + if (new Intent("eu.siacs.conversations.location.request").resolveActivity(getPackageManager()) == null) { + attachFilePopup.getMenu().findItem(R.id.attach_location).setVisible(false); + } attachFilePopup.setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override @@ -543,6 +552,9 @@ public class ConversationActivity extends XmppActivity case R.id.attach_record_voice: attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); break; + case R.id.attach_location: + attachFile(ATTACHMENT_CHOICE_LOCATION); + break; } return false; } @@ -677,8 +689,7 @@ public class ConversationActivity extends XmppActivity if (durations[which] == -1) { till = Long.MAX_VALUE; } else { - till = SystemClock.elapsedRealtime() - + (durations[which] * 1000); + till = System.currentTimeMillis() + (durations[which] * 1000); } conversation.setMutedTill(till); ConversationActivity.this.xmppConnectionService.databaseBackend @@ -722,6 +733,7 @@ public class ConversationActivity extends XmppActivity @Override public void onStart() { super.onStart(); + this.mRedirected = false; if (this.xmppConnectionServiceBound) { this.onBackendConnected(); } @@ -778,10 +790,19 @@ public class ConversationActivity extends XmppActivity this.xmppConnectionService.getNotificationService().setIsInForeground(true); updateConversationList(); if (xmppConnectionService.getAccounts().size() == 0) { - startActivity(new Intent(this, EditAccountActivity.class)); + if (!mRedirected) { + this.mRedirected = true; + startActivity(new Intent(this, EditAccountActivity.class)); + finish(); + } } else if (conversationList.size() <= 0) { - startActivity(new Intent(this, StartConversationActivity.class)); - finish(); + if (!mRedirected) { + this.mRedirected = true; + Intent intent = new Intent(this, StartConversationActivity.class); + intent.putExtra("init",true); + startActivity(intent); + finish(); + } } else if (getIntent() != null && VIEW_CONVERSATION.equals(getIntent().getType())) { handleViewConversationIntent(getIntent()); } else if (selectConversationByUuid(mOpenConverstaion)) { @@ -795,11 +816,12 @@ public class ConversationActivity extends XmppActivity this.mConversationFragment.reInit(getSelectedConversation()); mOpenConverstaion = null; } else if (getSelectedConversation() != null) { - this.mConversationFragment.updateMessages(); + this.mConversationFragment.reInit(getSelectedConversation()); } else { showConversationsOverview(); mPendingImageUri = null; mPendingFileUri = null; + mPendingGeoUri = null; setSelectedConversation(conversationList.get(0)); this.mConversationFragment.reInit(getSelectedConversation()); } @@ -810,6 +832,9 @@ public class ConversationActivity extends XmppActivity } else if (mPendingFileUri != null) { attachFileToConversation(getSelectedConversation(),mPendingFileUri); mPendingFileUri = null; + } else if (mPendingGeoUri != null) { + attachLocationToConversation(getSelectedConversation(),mPendingGeoUri); + mPendingGeoUri = null; } ExceptionHelper.checkForCrash(this, this.xmppConnectionService); setIntent(new Intent()); @@ -888,6 +913,14 @@ public class ConversationActivity extends XmppActivity Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(mPendingImageUri); sendBroadcast(intent); + } else if (requestCode == ATTACHMENT_CHOICE_LOCATION) { + double latitude = data.getDoubleExtra("latitude",0); + double longitude = data.getDoubleExtra("longitude",0); + this.mPendingGeoUri = Uri.parse("geo:"+String.valueOf(latitude)+","+String.valueOf(longitude)); + if (xmppConnectionServiceBound) { + attachLocationToConversation(getSelectedConversation(), mPendingGeoUri); + this.mPendingGeoUri = null; + } } } else { if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { @@ -896,6 +929,26 @@ public class ConversationActivity extends XmppActivity } } + private void attachLocationToConversation(Conversation conversation, Uri uri) { + xmppConnectionService.attachLocationToConversation(conversation,uri, new UiCallback<Message>() { + + @Override + public void success(Message message) { + xmppConnectionService.sendMessage(message); + } + + @Override + public void error(int errorCode, Message object) { + + } + + @Override + public void userInputRequried(PendingIntent pi, Message object) { + + } + }); + } + private void attachFileToConversation(Conversation conversation, Uri uri) { prepareFileToast = Toast.makeText(getApplicationContext(), getText(R.string.preparing_file), Toast.LENGTH_LONG); @@ -1008,56 +1061,50 @@ public class ConversationActivity extends XmppActivity } @Override - public void onAccountUpdate() { - runOnUiThread(new Runnable() { - - @Override - public void run() { - updateConversationList(); - ConversationActivity.this.mConversationFragment.updateMessages(); - updateActionBarTitle(); + protected void refreshUiReal() { + updateConversationList(); + if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 0) { + if (!mRedirected) { + this.mRedirected = true; + startActivity(new Intent(this, EditAccountActivity.class)); + finish(); } - }); + } else if (conversationList.size() == 0) { + if (!mRedirected) { + this.mRedirected = true; + Intent intent = new Intent(this, StartConversationActivity.class); + intent.putExtra("init",true); + startActivity(intent); + finish(); + } + } else { + ConversationActivity.this.mConversationFragment.updateMessages(); + updateActionBarTitle(); + } } @Override - public void onConversationUpdate() { - runOnUiThread(new Runnable() { + public void onAccountUpdate() { + this.refreshUi(); + } - @Override - public void run() { - updateConversationList(); - if (conversationList.size() == 0) { - startActivity(new Intent(getApplicationContext(), - StartConversationActivity.class)); - finish(); - } - ConversationActivity.this.mConversationFragment.updateMessages(); - updateActionBarTitle(); - } - }); + @Override + public void onConversationUpdate() { + this.refreshUi(); } @Override public void onRosterUpdate() { - runOnUiThread(new Runnable() { - - @Override - public void run() { - updateConversationList(); - ConversationActivity.this.mConversationFragment.updateMessages(); - updateActionBarTitle(); - } - }); + this.refreshUi(); } @Override public void OnUpdateBlocklist(Status status) { + this.refreshUi(); runOnUiThread(new Runnable() { @Override public void run() { invalidateOptionsMenu(); - ConversationActivity.this.mConversationFragment.updateMessages(); } }); } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 80ac9da1..d5f20e41 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -8,6 +8,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.content.IntentSender; import android.content.IntentSender.SendIntentException; +import android.net.Uri; import android.os.Bundle; import android.text.InputType; import android.view.ContextMenu; @@ -40,6 +41,7 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentLinkedQueue; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; @@ -52,15 +54,16 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.XmppConnectionService; -import eu.siacs.conversations.ui.EditMessage.OnEnterPressed; import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; import eu.siacs.conversations.ui.adapter.MessageAdapter; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureClicked; import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClicked; +import eu.siacs.conversations.utils.GeoHelper; +import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; -public class ConversationFragment extends Fragment { +public class ConversationFragment extends Fragment implements EditMessage.KeyboardListener { protected Conversation conversation; private OnClickListener leaveMuc = new OnClickListener() { @@ -99,7 +102,6 @@ public class ConversationFragment extends Fragment { protected ListView messagesView; final protected List<Message> messageList = new ArrayList<>(); protected MessageAdapter messageListAdapter; - protected Contact contact; private EditMessage mEditMessage; private ImageButton mSendButton; private RelativeLayout snackbar; @@ -196,6 +198,7 @@ public class ConversationFragment extends Fragment { askForPassphraseIntent, ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, 0, 0); + askForPassphraseIntent = null; } catch (SendIntentException e) { // } @@ -321,22 +324,12 @@ public class ConversationFragment extends Fragment { @Override public void onClick(View v) { - activity.hideConversationsOverview(); - } - }); - mEditMessage.setOnEditorActionListener(mEditorActionListener); - mEditMessage.setOnEnterPressedListener(new OnEnterPressed() { - - @Override - public boolean onEnterPressed() { - if (activity.enterIsSend()) { - sendMessage(); - return true; - } else { - return false; + if (activity != null) { + activity.hideConversationsOverview(); } } }); + mEditMessage.setOnEditorActionListener(mEditorActionListener); mSendButton = (ImageButton) view.findViewById(R.id.textSendButton); mSendButton.setOnClickListener(this.mSendButtonListener); @@ -418,19 +411,21 @@ public class ConversationFragment extends Fragment { MenuItem copyUrl = menu.findItem(R.id.copy_url); MenuItem downloadImage = menu.findItem(R.id.download_image); MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); - if (m.getType() != Message.TYPE_TEXT || m.getDownloadable() != null) { + if ((m.getType() != Message.TYPE_TEXT && m.getType() != Message.TYPE_PRIVATE) + || m.getDownloadable() != null || GeoHelper.isGeoUri(m.getBody())) { copyText.setVisible(false); } - if (m.getType() == Message.TYPE_TEXT + if ((m.getType() == Message.TYPE_TEXT || m.getType() == Message.TYPE_PRIVATE - || m.getDownloadable() != null) { + || m.getDownloadable() != null) + && (!GeoHelper.isGeoUri(m.getBody()))) { shareWith.setVisible(false); - } + } if (m.getStatus() != Message.STATUS_SEND_FAILED) { sendAgain.setVisible(false); } - if ((m.getType() != Message.TYPE_IMAGE && m.getDownloadable() == null) - || m.getImageParams().url == null) { + if (((m.getType() != Message.TYPE_IMAGE && m.getDownloadable() == null) + || m.getImageParams().url == null) && !GeoHelper.isGeoUri(m.getBody())) { copyUrl.setVisible(false); } if (m.getType() != Message.TYPE_TEXT @@ -475,16 +470,21 @@ public class ConversationFragment extends Fragment { private void shareWith(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); - String path = message.getRelativeFilePath(); - String mime = path == null ? null :URLConnection.guessContentTypeFromName(path); - if (mime == null) { - mime = "image/webp"; - } - shareIntent.setType(mime); + if (GeoHelper.isGeoUri(message.getBody())) { + shareIntent.putExtra(Intent.EXTRA_TEXT, message.getBody()); + shareIntent.setType("text/plain"); + } else { + shareIntent.putExtra(Intent.EXTRA_STREAM, + activity.xmppConnectionService.getFileBackend() + .getJingleFileUri(message)); + shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + String path = message.getRelativeFilePath(); + String mime = path == null ? null : URLConnection.guessContentTypeFromName(path); + if (mime == null) { + mime = "image/webp"; + } + shareIntent.setType(mime); + } activity.startActivity(Intent.createChooser(shareIntent,getText(R.string.share_with))); } @@ -509,8 +509,16 @@ public class ConversationFragment extends Fragment { } private void copyUrl(Message message) { - if (activity.copyTextToClipboard( - message.getImageParams().url.toString(), R.string.image_url)) { + final String url; + final int resId; + if (GeoHelper.isGeoUri(message.getBody())) { + resId = R.string.location; + url = message.getBody(); + } else { + resId = R.string.image_url; + url = message.getImageParams().url.toString(); + } + if (activity.copyTextToClipboard(url, resId)) { Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } @@ -555,7 +563,17 @@ public class ConversationFragment extends Fragment { mDecryptJobRunning = false; super.onStop(); if (this.conversation != null) { - this.conversation.setNextMessage(mEditMessage.getText().toString()); + final String msg = mEditMessage.getText().toString(); + this.conversation.setNextMessage(msg); + updateChatState(this.conversation,msg); + } + } + + private void updateChatState(final Conversation conversation, final String msg) { + ChatState state = msg.length() == 0 ? Config.DEFAULT_CHATSTATE : ChatState.PAUSED; + Account.State status = conversation.getAccount().getStatus(); + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(state)) { + activity.xmppConnectionService.sendChatState(conversation); } } @@ -563,20 +581,30 @@ public class ConversationFragment extends Fragment { if (conversation == null) { return; } + + this.activity = (ConversationActivity) getActivity(); + if (this.conversation != null) { - this.conversation.setNextMessage(mEditMessage.getText().toString()); + final String msg = mEditMessage.getText().toString(); + this.conversation.setNextMessage(msg); + if (this.conversation != conversation) { + updateChatState(this.conversation,msg); + } this.conversation.trim(); } - this.activity = (ConversationActivity) getActivity(); + + this.askForPassphraseIntent = null; this.conversation = conversation; this.mDecryptJobRunning = false; this.mEncryptedMessages.clear(); if (this.conversation.getMode() == Conversation.MODE_MULTI) { this.conversation.setNextCounterpart(null); } + this.mEditMessage.setKeyboardListener(null); this.mEditMessage.setText(""); this.mEditMessage.append(this.conversation.getNextMessage()); - this.messagesView.invalidateViews(); + this.mEditMessage.setKeyboardListener(this); + this.messagesView.setAdapter(messageListAdapter); updateMessages(); this.messagesLoaded = true; int size = this.messageList.size(); @@ -585,88 +613,127 @@ public class ConversationFragment extends Fragment { } } + private OnClickListener mUnblockClickListener = new OnClickListener() { + @Override + public void onClick(final View v) { + v.post(new Runnable() { + @Override + public void run() { + v.setVisibility(View.INVISIBLE); + } + }); + if (conversation.isDomainBlocked()) { + BlockContactDialog.show(activity, activity.xmppConnectionService, conversation); + } else { + activity.unblockConversation(conversation); + } + } + }; + + private OnClickListener mAddBackClickListener = new OnClickListener() { + + @Override + public void onClick(View v) { + final Contact contact = conversation == null ? null :conversation.getContact(); + if (contact != null) { + activity.xmppConnectionService.createContact(contact); + activity.switchToContactDetails(contact); + } + } + }; + + private OnClickListener mUnmuteClickListener = new OnClickListener() { + + @Override + public void onClick(final View v) { + activity.unmuteConversation(conversation); + } + }; + + private OnClickListener mAnswerSmpClickListener = new OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(activity, VerifyOTRActivity.class); + intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); + intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); + intent.putExtra("mode",VerifyOTRActivity.MODE_ANSWER_QUESTION); + startActivity(intent); + } + }; + + private void updateSnackBar(final Conversation conversation) { + final Account account = conversation.getAccount(); + final Contact contact = conversation.getContact(); + final int mode = conversation.getMode(); + if (conversation.isBlocked()) { + showSnackbar(R.string.contact_blocked, R.string.unblock,this.mUnblockClickListener); + } else if (!contact.showInRoster() && contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { + showSnackbar(R.string.contact_added_you, R.string.add_back,this.mAddBackClickListener); + } else if (mode == Conversation.MODE_MULTI + &&!conversation.getMucOptions().online() + && account.getStatus() == Account.State.ONLINE) { + switch (conversation.getMucOptions().getError()) { + case MucOptions.ERROR_NICK_IN_USE: + showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc); + break; + case MucOptions.ERROR_UNKNOWN: + showSnackbar(R.string.conference_not_found, R.string.leave, leaveMuc); + break; + case MucOptions.ERROR_PASSWORD_REQUIRED: + showSnackbar(R.string.conference_requires_password, R.string.enter_password, enterPassword); + break; + case MucOptions.ERROR_BANNED: + showSnackbar(R.string.conference_banned, R.string.leave, leaveMuc); + break; + case MucOptions.ERROR_MEMBERS_ONLY: + showSnackbar(R.string.conference_members_only, R.string.leave, leaveMuc); + break; + case MucOptions.KICKED_FROM_ROOM: + showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); + break; + default: + break; + } + } else if (askForPassphraseIntent != null ) { + showSnackbar(R.string.openpgp_messages_found,R.string.decrypt, clickToDecryptListener); + } else if (mode == Conversation.MODE_SINGLE + && conversation.smpRequested()) { + showSnackbar(R.string.smp_requested, R.string.verify,this.mAnswerSmpClickListener); + } else if (mode == Conversation.MODE_SINGLE + &&conversation.hasValidOtrSession() + && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) + && (!conversation.isOtrFingerprintVerified())) { + showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); + } else if (conversation.isMuted()) { + showSnackbar(R.string.notifications_disabled, R.string.enable,this.mUnmuteClickListener); + } else { + hideSnackbar(); + } + } + public void updateMessages() { synchronized (this.messageList) { if (getView() == null) { return; } - hideSnackbar(); final ConversationActivity activity = (ConversationActivity) getActivity(); if (this.conversation != null) { + updateSnackBar(this.conversation); final Contact contact = this.conversation.getContact(); if (this.conversation.isBlocked()) { - showSnackbar(R.string.contact_blocked, R.string.unblock, - new OnClickListener() { - @Override - public void onClick(final View v) { - v.post(new Runnable() { - @Override - public void run() { - v.setVisibility(View.INVISIBLE); - } - }); - if (conversation.isDomainBlocked()) { - BlockContactDialog.show(getActivity(), ((ConversationActivity) getActivity()).xmppConnectionService, conversation); - } else { - ((ConversationActivity) getActivity()).unblockConversation(conversation); - } - } - }); + } else if (!contact.showInRoster() && contact .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { - showSnackbar(R.string.contact_added_you, R.string.add_back, - new OnClickListener() { - @Override - public void onClick(View v) { - activity.xmppConnectionService - .createContact(contact); - activity.switchToContactDetails(contact); - } - }); } else if (conversation.getMode() == Conversation.MODE_SINGLE) { makeFingerprintWarning(); } else if (!conversation.getMucOptions().online() && conversation.getAccount().getStatus() == Account.State.ONLINE) { - int error = conversation.getMucOptions().getError(); - switch (error) { - case MucOptions.ERROR_NICK_IN_USE: - showSnackbar(R.string.nick_in_use, R.string.edit, - clickToMuc); - break; - case MucOptions.ERROR_UNKNOWN: - showSnackbar(R.string.conference_not_found, - R.string.leave, leaveMuc); - break; - case MucOptions.ERROR_PASSWORD_REQUIRED: - showSnackbar(R.string.conference_requires_password, - R.string.enter_password, enterPassword); - break; - case MucOptions.ERROR_BANNED: - showSnackbar(R.string.conference_banned, - R.string.leave, leaveMuc); - break; - case MucOptions.ERROR_MEMBERS_ONLY: - showSnackbar(R.string.conference_members_only, - R.string.leave, leaveMuc); - break; - case MucOptions.KICKED_FROM_ROOM: - showSnackbar(R.string.conference_kicked, R.string.join, - joinMuc); - break; - default: - break; - } + } else if (this.conversation.isMuted()) { - showSnackbar(R.string.notifications_disabled, R.string.enable, - new OnClickListener() { - @Override - public void onClick(final View v) { - activity.unmuteConversation(conversation); - } - }); } conversation.populateWithMessages(ConversationFragment.this.messageList); for (final Message message : this.messageList) { @@ -703,8 +770,7 @@ public class ConversationFragment extends Fragment { public void userInputRequried(PendingIntent pi, Message message) { mDecryptJobRunning = false; askForPassphraseIntent = pi.getIntentSender(); - showSnackbar(R.string.openpgp_messages_found, - R.string.decrypt, clickToDecryptListener); + updateSnackBar(conversation); } @Override @@ -792,13 +858,21 @@ public class ConversationFragment extends Fragment { protected void updateStatusMessages() { synchronized (this.messageList) { if (conversation.getMode() == Conversation.MODE_SINGLE) { - for (int i = this.messageList.size() - 1; i >= 0; --i) { - if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) { - return; - } else { - if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { - this.messageList.add(i + 1,Message.createStatusMessage(conversation)); + ChatState state = conversation.getIncomingChatState(); + if (state == ChatState.COMPOSING) { + this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_is_typing, conversation.getName()))); + } else if (state == ChatState.PAUSED) { + this.messageList.add(Message.createStatusMessage(conversation, getString(R.string.contact_has_stopped_typing, conversation.getName()))); + } else { + for (int i = this.messageList.size() - 1; i >= 0; --i) { + if (this.messageList.get(i).getStatus() == Message.STATUS_RECEIVED) { return; + } else { + if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { + this.messageList.add(i + 1, + Message.createStatusMessage(conversation, getString(R.string.contact_has_read_up_to_this_point, conversation.getName()))); + return; + } } } } @@ -807,22 +881,7 @@ public class ConversationFragment extends Fragment { } protected void makeFingerprintWarning() { - if (conversation.smpRequested()) { - showSnackbar(R.string.smp_requested, R.string.verify, new OnClickListener() { - @Override - public void onClick(View view) { - Intent intent = new Intent(activity, VerifyOTRActivity.class); - intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); - intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); - intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); - intent.putExtra("mode",VerifyOTRActivity.MODE_ANSWER_QUESTION); - startActivity(intent); - } - }); - } else if (conversation.hasValidOtrSession() && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) - && (!conversation.isOtrFingerprintVerified())) { - showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); - } + } protected void showSnackbar(final int message, final int action, @@ -968,4 +1027,38 @@ public class ConversationFragment extends Fragment { this.mEditMessage.append(text); } + @Override + public boolean onEnterPressed() { + if (activity.enterIsSend()) { + sendMessage(); + return true; + } else { + return false; + } + } + + @Override + public void onTypingStarted() { + Account.State status = conversation.getAccount().getStatus(); + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.COMPOSING)) { + activity.xmppConnectionService.sendChatState(conversation); + } + } + + @Override + public void onTypingStopped() { + Account.State status = conversation.getAccount().getStatus(); + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(ChatState.PAUSED)) { + activity.xmppConnectionService.sendChatState(conversation); + } + } + + @Override + public void onTextDeleted() { + Account.State status = conversation.getAccount().getStatus(); + if (status == Account.State.ONLINE && conversation.setOutgoingChatState(Config.DEFAULT_CHATSTATE)) { + activity.xmppConnectionService.sendChatState(conversation); + } + } + } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index ea45b75e..27dfc492 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -102,6 +102,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } catch (final InvalidJidException ignored) { return; } + mAccountJid.setError(null); + mPasswordConfirm.setError(null); mAccount.setPassword(password); mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); xmppConnectionService.updateAccount(mAccount); @@ -221,6 +223,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate if (avatar != null) { intent = new Intent(getApplicationContext(), StartConversationActivity.class); + intent.putExtra("init",true); } else { intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); @@ -234,7 +237,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } protected void updateSaveButton() { - if (mAccount != null && mAccount.getStatus() == Account.State.CONNECTING) { + if (mAccount != null && (mAccount.getStatus() == Account.State.CONNECTING || mFetchingAvatar)) { this.mSaveButton.setEnabled(false); this.mSaveButton.setTextColor(getSecondaryTextColor()); this.mSaveButton.setText(R.string.account_status_connecting); @@ -329,17 +332,18 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list); final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more); final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server); - if (mAccount == null) { + if (mAccount != null && mAccount.isOnlineAndConnected()) { + if (!mAccount.getXmppConnection().getFeatures().blocking()) { + showBlocklist.setVisible(false); + } + if (!mAccount.getXmppConnection().getFeatures().register()) { + changePassword.setVisible(false); + } + } else { showQrCode.setVisible(false); showBlocklist.setVisible(false); showMoreInfo.setVisible(false); changePassword.setVisible(false); - } else if (mAccount.getStatus() != Account.State.ONLINE) { - showBlocklist.setVisible(false); - showMoreInfo.setVisible(false); - changePassword.setVisible(false); - } else if (!mAccount.getXmppConnection().getFeatures().blocking()) { - showBlocklist.setVisible(false); } return true; } @@ -379,6 +383,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate if (getActionBar() != null) { getActionBar().setDisplayHomeAsUpEnabled(false); getActionBar().setDisplayShowHomeEnabled(false); + getActionBar().setHomeButtonEnabled(false); } this.mCancelButton.setEnabled(false); this.mCancelButton.setTextColor(getSecondaryTextColor()); @@ -491,6 +496,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate if (this.mAccount.errorStatus()) { this.mAccountJid.setError(getString(this.mAccount.getStatus().getReadableId())); this.mAccountJid.requestFocus(); + } else { + this.mAccountJid.setError(null); } this.mStats.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java index 5090bbf5..a58cf2b8 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java @@ -1,10 +1,13 @@ package eu.siacs.conversations.ui; import android.content.Context; +import android.os.Handler; import android.util.AttributeSet; import android.view.KeyEvent; import android.widget.EditText; +import eu.siacs.conversations.Config; + public class EditMessage extends EditText { public EditMessage(Context context, AttributeSet attrs) { @@ -15,28 +18,61 @@ public class EditMessage extends EditText { super(context); } - protected OnEnterPressed mOnEnterPressed; + protected Handler mTypingHandler = new Handler(); + + protected Runnable mTypingTimeout = new Runnable() { + @Override + public void run() { + if (isUserTyping && keyboardListener != null) { + keyboardListener.onTypingStopped(); + isUserTyping = false; + } + } + }; + + private boolean isUserTyping = false; + + protected KeyboardListener keyboardListener; @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_ENTER) { - if (mOnEnterPressed != null) { - if (mOnEnterPressed.onEnterPressed()) { - return true; - } else { - return super.onKeyDown(keyCode, event); - } + if (keyboardListener != null && keyboardListener.onEnterPressed()) { + return true; } } return super.onKeyDown(keyCode, event); } - public void setOnEnterPressedListener(OnEnterPressed listener) { - this.mOnEnterPressed = listener; + @Override + public void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { + super.onTextChanged(text,start,lengthBefore,lengthAfter); + if (this.mTypingHandler != null && this.keyboardListener != null) { + this.mTypingHandler.removeCallbacks(mTypingTimeout); + this.mTypingHandler.postDelayed(mTypingTimeout, Config.TYPING_TIMEOUT * 1000); + final int length = text.length(); + if (!isUserTyping && length > 0) { + this.isUserTyping = true; + this.keyboardListener.onTypingStarted(); + } else if (length == 0) { + this.isUserTyping = false; + this.keyboardListener.onTextDeleted(); + } + } + } + + public void setKeyboardListener(KeyboardListener listener) { + this.keyboardListener = listener; + if (listener != null) { + this.isUserTyping = false; + } } - public interface OnEnterPressed { + public interface KeyboardListener { public boolean onEnterPressed(); + public void onTypingStarted(); + public void onTypingStopped(); + public void onTextDeleted(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java index b3ab5ee6..b2d5ddfd 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -32,18 +32,17 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public void onAccountUpdate() { + refreshUi(); + } + + @Override + protected void refreshUiReal() { synchronized (this.accountList) { accountList.clear(); accountList.addAll(xmppConnectionService.getAccounts()); } - runOnUiThread(new Runnable() { - - @Override - public void run() { - invalidateOptionsMenu(); - mAccountAdapter.notifyDataSetChanged(); - } - }); + invalidateOptionsMenu(); + mAccountAdapter.notifyDataSetChanged(); } @Override diff --git a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java index 2ba0b090..3f72b723 100644 --- a/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/PublishProfilePictureActivity.java @@ -54,8 +54,10 @@ public class PublishProfilePictureActivity extends XmppActivity { @Override public void run() { if (mInitialAccountSetup) { - startActivity(new Intent(getApplicationContext(), - StartConversationActivity.class)); + Intent intent = new Intent(getApplicationContext(), + StartConversationActivity.class); + intent.putExtra("init",true); + startActivity(intent); } Toast.makeText(PublishProfilePictureActivity.this, R.string.avatar_has_been_published, @@ -112,8 +114,10 @@ public class PublishProfilePictureActivity extends XmppActivity { @Override public void onClick(View v) { if (mInitialAccountSetup) { - startActivity(new Intent(getApplicationContext(), - StartConversationActivity.class)); + Intent intent = new Intent(getApplicationContext(), + StartConversationActivity.class); + intent.putExtra("init",true); + startActivity(intent); } finish(); } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 136108ef..1c1ff3b9 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -67,7 +67,7 @@ public class SettingsActivity extends XmppActivity implements for (Account account : xmppConnectionService.getAccounts()) { account.setResource(resource); if (!account.isOptionSet(Account.OPTION_DISABLED)) { - xmppConnectionService.reconnectAccount(account, false); + xmppConnectionService.reconnectAccountInBackground(account); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index ff46ffd8..a556b8b7 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -176,15 +176,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU @Override public void onRosterUpdate() { - runOnUiThread(new Runnable() { - - @Override - public void run() { - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } - } - }); + this.refreshUi(); } @Override @@ -582,9 +574,15 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU this.mActivatedAccounts.add(account.getJid().toBareJid().toString()); } } + final Intent intent = getIntent(); + final ActionBar ab = getActionBar(); + if (intent != null && intent.getBooleanExtra("init",false) && ab != null) { + ab.setDisplayShowHomeEnabled(false); + ab.setDisplayHomeAsUpEnabled(false); + ab.setHomeButtonEnabled(false); + } this.mKnownHosts = xmppConnectionService.getKnownHosts(); - this.mKnownConferenceHosts = xmppConnectionService - .getKnownConferenceHosts(); + this.mKnownConferenceHosts = xmppConnectionService.getKnownConferenceHosts(); if (this.mPendingInvite != null) { mPendingInvite.invite(); this.mPendingInvite = null; @@ -711,15 +709,14 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU @Override public void OnUpdateBlocklist(final Status status) { - runOnUiThread(new Runnable() { + refreshUi(); + } - @Override - public void run() { - if (mSearchEditText != null) { - filter(mSearchEditText.getText().toString()); - } - } - }); + @Override + protected void refreshUiReal() { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } } public static class MyListFragment extends ListFragment { diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java index c33decd8..ec9d59e1 100644 --- a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java @@ -436,11 +436,11 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer } public void onConversationUpdate() { - runOnUiThread(new Runnable() { - @Override - public void run() { - updateView(); - } - }); + refreshUi(); + } + + @Override + protected void refreshUiReal() { + updateView(); } } diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 1f1af09c..62f62b9a 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -33,7 +33,9 @@ import android.nfc.NfcEvent; import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; +import android.os.Handler; import android.os.IBinder; +import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.InputType; import android.util.DisplayMetrics; @@ -68,6 +70,7 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; @@ -100,6 +103,33 @@ public abstract class XmppActivity extends Activity { protected int mTheme; protected boolean mUsingEnterKey = false; + private long mLastUiRefresh = 0; + private Handler mRefreshUiHandler = new Handler(); + private Runnable mRefreshUiRunnable = new Runnable() { + @Override + public void run() { + mLastUiRefresh = SystemClock.elapsedRealtime(); + refreshUiReal(); + } + }; + + + protected void refreshUi() { + final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh; + if (diff > Config.REFRESH_UI_INTERVAL) { + mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); + runOnUiThread(mRefreshUiRunnable); + } else { + final long next = Config.REFRESH_UI_INTERVAL - diff; + mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable); + mRefreshUiHandler.postDelayed(mRefreshUiRunnable,next); + } + } + + protected void refreshUiReal() { + + }; + protected interface OnValueEdited { public void onValueEdited(String value); } @@ -384,7 +414,20 @@ public abstract class XmppActivity extends Activity { protected void inviteToConversation(Conversation conversation) { Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class); + List<String> contacts = new ArrayList<>(); + if (conversation.getMode() == Conversation.MODE_MULTI) { + for (MucOptions.User user : conversation.getMucOptions().getUsers()) { + Jid jid = user.getJid(); + if (jid != null) { + contacts.add(jid.toBareJid().toString()); + } + } + } else { + contacts.add(conversation.getJid().toBareJid().toString()); + } + intent.putExtra("filter_contacts", contacts.toArray(new String[contacts.size()])); intent.putExtra("conversation", conversation.getUuid()); + intent.putExtra("multiple", true); startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION); } @@ -627,22 +670,31 @@ public abstract class XmppActivity extends Activity { if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) { try { - Jid jid = Jid.fromString(data.getStringExtra("contact")); String conversationUuid = data.getStringExtra("conversation"); Conversation conversation = xmppConnectionService .findConversationByUuid(conversationUuid); + List<Jid> jids = new ArrayList<Jid>(); + if (data.getBooleanExtra("multiple", false)) { + String[] toAdd = data.getStringArrayExtra("contacts"); + for (String item : toAdd) { + jids.add(Jid.fromString(item)); + } + } else { + jids.add(Jid.fromString(data.getStringExtra("contact"))); + } + if (conversation.getMode() == Conversation.MODE_MULTI) { - xmppConnectionService.invite(conversation, jid); + for (Jid jid : jids) { + xmppConnectionService.invite(conversation, jid); + } } else { - List<Jid> jids = new ArrayList<Jid>(); jids.add(conversation.getJid().toBareJid()); - jids.add(jid); xmppConnectionService.createAdhocConference(conversation.getAccount(), jids, adhocCallback); } } catch (final InvalidJidException ignored) { } - } + } } private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() { 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 e62aaf96..c4a446e8 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -1,8 +1,13 @@ package eu.siacs.conversations.ui.adapter; import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Typeface; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; import android.util.Pair; import android.view.LayoutInflater; import android.view.View; @@ -11,14 +16,15 @@ import android.widget.ArrayAdapter; import android.widget.ImageView; import android.widget.TextView; +import java.lang.ref.WeakReference; import java.util.List; +import java.util.concurrent.RejectedExecutionException; import de.tzur.conversations.Settings; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Downloadable; -import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.ui.ConversationActivity; @@ -132,8 +138,91 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { mTimestamp.setText(UIHelper.readableTimeDifference(activity,conversation.getLatestMessage().getTimeSent())); ImageView profilePicture = (ImageView) view.findViewById(R.id.conversation_image); - profilePicture.setImageBitmap(activity.avatarService().get(conversation, activity.getPixel(56))); + loadAvatar(conversation,profilePicture); return view; } -} + + class BitmapWorkerTask extends AsyncTask<Conversation, Void, Bitmap> { + private final WeakReference<ImageView> imageViewReference; + private Conversation conversation = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + } + + @Override + protected Bitmap doInBackground(Conversation... params) { + return activity.avatarService().get(params[0], activity.getPixel(56)); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + public void loadAvatar(Conversation conversation, ImageView imageView) { + if (cancelPotentialWork(conversation, imageView)) { + final Bitmap bm = activity.avatarService().get(conversation, activity.getPixel(56), true); + if (bm != null) { + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + imageView.setBackgroundColor(UIHelper.getColorForName(conversation.getName())); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(conversation); + } catch (final RejectedExecutionException ignored) { + } + } + } + } + + public static boolean cancelPotentialWork(Conversation conversation, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final Conversation oldConversation = bitmapWorkerTask.conversation; + if (oldConversation == null || conversation != oldConversation) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } +}
\ No newline at end of file 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 91fb021c..7b20b55f 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -1,17 +1,23 @@ package eu.siacs.conversations.ui.adapter; +import java.lang.ref.WeakReference; import java.util.List; +import java.util.concurrent.RejectedExecutionException; -import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.ui.XmppActivity; +import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xmpp.jid.Jid; import android.content.Context; import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.AsyncTask; import android.preference.PreferenceManager; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -77,8 +83,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { tvJid.setText(""); } tvName.setText(item.getDisplayName()); - picture.setImageBitmap(activity.avatarService().get(item, - activity.getPixel(48))); + loadAvatar(item,picture); return view; } @@ -90,4 +95,87 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { public void onTagClicked(String tag); } + class BitmapWorkerTask extends AsyncTask<ListItem, Void, Bitmap> { + private final WeakReference<ImageView> imageViewReference; + private ListItem item = null; + + public BitmapWorkerTask(ImageView imageView) { + imageViewReference = new WeakReference<>(imageView); + } + + @Override + protected Bitmap doInBackground(ListItem... params) { + return activity.avatarService().get(params[0], activity.getPixel(48)); + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + if (bitmap != null) { + final ImageView imageView = imageViewReference.get(); + if (imageView != null) { + imageView.setImageBitmap(bitmap); + imageView.setBackgroundColor(0x00000000); + } + } + } + } + + public void loadAvatar(ListItem item, ImageView imageView) { + if (cancelPotentialWork(item, imageView)) { + final Bitmap bm = activity.avatarService().get(item,activity.getPixel(48),true); + if (bm != null) { + imageView.setImageBitmap(bm); + imageView.setBackgroundColor(0x00000000); + } else { + imageView.setBackgroundColor(UIHelper.getColorForName(item.getDisplayName())); + imageView.setImageDrawable(null); + final BitmapWorkerTask task = new BitmapWorkerTask(imageView); + final AsyncDrawable asyncDrawable = new AsyncDrawable(activity.getResources(), null, task); + imageView.setImageDrawable(asyncDrawable); + try { + task.execute(item); + } catch (final RejectedExecutionException ignored) { + } + } + } + } + + public static boolean cancelPotentialWork(ListItem item, ImageView imageView) { + final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); + + if (bitmapWorkerTask != null) { + final ListItem oldItem = bitmapWorkerTask.item; + if (oldItem == null || item != oldItem) { + bitmapWorkerTask.cancel(true); + } else { + return false; + } + } + return true; + } + + private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) { + if (imageView != null) { + final Drawable drawable = imageView.getDrawable(); + if (drawable instanceof AsyncDrawable) { + final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable; + return asyncDrawable.getBitmapWorkerTask(); + } + } + return null; + } + + static class AsyncDrawable extends BitmapDrawable { + private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; + + public AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) { + super(res, bitmap); + bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask); + } + + public BitmapWorkerTask getBitmapWorkerTask() { + return bitmapWorkerTaskReference.get(); + } + } + } 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 7fa05050..c3736f0b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -36,6 +36,7 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message.ImageParams; import eu.siacs.conversations.ui.ConversationActivity; +import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; public class MessageAdapter extends ArrayAdapter<Message> { @@ -305,6 +306,21 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.download_button.setOnLongClickListener(openContextMenu); } + private void displayLocationMessage(ViewHolder viewHolder, final Message message) { + viewHolder.image.setVisibility(View.GONE); + viewHolder.messageBody.setVisibility(View.GONE); + viewHolder.download_button.setVisibility(View.VISIBLE); + viewHolder.download_button.setText(R.string.show_location); + viewHolder.download_button.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + showLocation(message); + } + }); + viewHolder.download_button.setOnLongClickListener(openContextMenu); + } + private void displayImageMessage(ViewHolder viewHolder, final Message message) { if (viewHolder.download_button != null) { @@ -416,9 +432,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { .avatarService().get(conversation.getContact(), activity.getPixel(32))); viewHolder.contact_picture.setAlpha(0.5f); - viewHolder.status_message.setText( - activity.getString(R.string.contact_has_read_up_to_this_point, conversation.getName())); - + viewHolder.status_message.setText(message.getBody()); } return view; } else if (type == NULL) { @@ -517,7 +531,11 @@ public class MessageAdapter extends ArrayAdapter<Message> { } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { displayDecryptionFailed(viewHolder); } else { - displayTextMessage(viewHolder, message); + if (GeoHelper.isGeoUri(message.getBody())) { + displayLocationMessage(viewHolder,message); + } else { + displayTextMessage(viewHolder, message); + } } displayStatus(viewHolder, message); @@ -552,6 +570,16 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } + public void showLocation(Message message) { + for(Intent intent : GeoHelper.createGeoIntentsFromMessage(message)) { + if (intent.resolveActivity(getContext().getPackageManager()) != null) { + getContext().startActivity(intent); + return; + } + } + Toast.makeText(activity,R.string.no_application_found_to_display_location,Toast.LENGTH_SHORT).show(); + } + public interface OnContactPictureClicked { public void onContactPictureClicked(Message message); } diff --git a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java index fc21acbc..eb7e2c3c 100644 --- a/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/CryptoHelper.java @@ -4,7 +4,9 @@ import java.security.SecureRandom; import java.text.Normalizer; import java.util.Arrays; import java.util.Collection; +import java.util.Iterator; import java.util.LinkedHashSet; +import java.util.List; import eu.siacs.conversations.Config; @@ -97,10 +99,26 @@ public final class CryptoHelper { return builder.toString(); } - public static String[] getSupportedCipherSuites(final String[] platformSupportedCipherSuites) { - //final Collection<String> cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS)); - //cipherSuites.retainAll(Arrays.asList(platformSupportedCipherSuites)); - //return cipherSuites.toArray(new String[cipherSuites.size()]); - return platformSupportedCipherSuites; + public static String[] getOrderedCipherSuites(final String[] platformSupportedCipherSuites) { + final Collection<String> cipherSuites = new LinkedHashSet<>(Arrays.asList(Config.ENABLED_CIPHERS)); + final List<String> platformCiphers = Arrays.asList(platformSupportedCipherSuites); + cipherSuites.retainAll(platformCiphers); + cipherSuites.addAll(platformCiphers); + filterWeakCipherSuites(cipherSuites); + return cipherSuites.toArray(new String[cipherSuites.size()]); + } + + private static void filterWeakCipherSuites(final Collection<String> cipherSuites) { + final Iterator<String> it = cipherSuites.iterator(); + while (it.hasNext()) { + String cipherName = it.next(); + // remove all ciphers with no or very weak encryption or no authentication + for (String weakCipherPattern : Config.WEAK_CIPHER_PATTERNS) { + if (cipherName.contains(weakCipherPattern)) { + it.remove(); + break; + } + } + } } } diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index a09b4d0f..bcb2ca44 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -140,18 +140,13 @@ public class DNSHelper { } ArrayList<Bundle> values = new ArrayList<>(); for (SRV srv : result) { - boolean added = false; if (ips6.containsKey(srv.getName())) { values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6)); - added = true; } if (ips4.containsKey(srv.getName())) { values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4)); - added = true; - } - if (!added) { - values.add(createNamePortBundle(srv.getName(),srv.getPort(),null)); } + values.add(createNamePortBundle(srv.getName(),srv.getPort(),null)); } bundle.putParcelableArrayList("values", values); } catch (SocketTimeoutException e) { diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java new file mode 100644 index 00000000..f7dda936 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java @@ -0,0 +1,71 @@ +package eu.siacs.conversations.utils; + +import android.content.Intent; +import android.net.Uri; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; + +public class GeoHelper { + private static Pattern GEO_URI = Pattern.compile("geo:([\\-0-9.]+),([\\-0-9.]+)(?:,([\\-0-9.]+))?(?:\\?(.*))?", Pattern.CASE_INSENSITIVE); + + public static boolean isGeoUri(String body) { + return body != null && GEO_URI.matcher(body).matches(); + } + + public static ArrayList<Intent> createGeoIntentsFromMessage(Message message) { + final ArrayList<Intent> intents = new ArrayList(); + Matcher matcher = GEO_URI.matcher(message.getBody()); + if (!matcher.matches()) { + return intents; + } + double latitude; + double longitude; + try { + latitude = Double.parseDouble(matcher.group(1)); + if (latitude > 90.0 || latitude < -90.0) { + return intents; + } + longitude = Double.parseDouble(matcher.group(2)); + if (longitude > 180.0 || longitude < -180.0) { + return intents; + } + } catch (NumberFormatException nfe) { + return intents; + } + final Conversation conversation = message.getConversation(); + String label; + if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { + try { + label = "(" + URLEncoder.encode(message.getConversation().getName(), "UTF-8") + ")"; + } catch (UnsupportedEncodingException e) { + label = ""; + } + } else { + label = ""; + } + + Intent locationPluginIntent = new Intent("eu.siacs.conversations.location.show"); + locationPluginIntent.putExtra("latitude",latitude); + locationPluginIntent.putExtra("longitude",longitude); + if (conversation.getMode() == Conversation.MODE_SINGLE && message.getStatus() == Message.STATUS_RECEIVED) { + locationPluginIntent.putExtra("name",conversation.getName()); + } + intents.add(locationPluginIntent); + + Intent geoIntent = new Intent(Intent.ACTION_VIEW); + geoIntent.setData(Uri.parse("geo:" + String.valueOf(latitude) + "," + String.valueOf(longitude) + "?q=" + String.valueOf(latitude) + "," + String.valueOf(longitude) + label)); + intents.add(geoIntent); + + Intent httpIntent = new Intent(Intent.ACTION_VIEW); + httpIntent.setData(Uri.parse("https://maps.google.com/maps?q=loc:"+String.valueOf(latitude) + "," + String.valueOf(longitude) +label)); + intents.add(httpIntent); + return intents; + } +} diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index 9a5cbaaf..99e8ebb8 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -8,6 +8,7 @@ import android.content.Context; import android.content.CursorLoader; import android.content.Loader; import android.content.Loader.OnLoadCompleteListener; +import android.content.pm.PackageManager; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; @@ -91,4 +92,17 @@ public class PhoneHelper { } } } + + public static String getVersionName(Context context) { + final String packageName = context == null ? null : context.getPackageName(); + if (packageName != null) { + try { + return context.getPackageManager().getPackageInfo(packageName, 0).versionName; + } catch (final PackageManager.NameNotFoundException e) { + return "unknown"; + } + } else { + return "unknown"; + } + } } diff --git a/src/main/java/eu/siacs/conversations/utils/UIHelper.java b/src/main/java/eu/siacs/conversations/utils/UIHelper.java index 333f6e27..0ddf606f 100644 --- a/src/main/java/eu/siacs/conversations/utils/UIHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/UIHelper.java @@ -295,8 +295,14 @@ public class UIHelper { if (message.getBody().startsWith(Message.ME_COMMAND)) { return new Pair<>(message.getBody().replaceAll("^" + Message.ME_COMMAND, UIHelper.getMessageDisplayName(message) + " "), false); - } else { - return new Pair<>(message.getBody(), false); + } else if (GeoHelper.isGeoUri(message.getBody())) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + return new Pair<>(context.getString(R.string.received_location),true); + } else { + return new Pair<>(context.getString(R.string.location), true); + } + } else{ + return new Pair<>(message.getBody().trim(), false); } } } diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index 0f1b18c3..92c0241e 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -32,7 +32,7 @@ public class XmppUri { protected void parse(Uri uri) { String scheme = uri.getScheme(); - if ("xmpp".equals(scheme)) { + if ("xmpp".equalsIgnoreCase(scheme)) { // sample: xmpp:jid@foo.com muc = "join".equalsIgnoreCase(uri.getQuery()); if (uri.getAuthority() != null) { @@ -41,7 +41,7 @@ public class XmppUri { jid = uri.getSchemeSpecificPart().split("\\?")[0]; } fingerprint = parseFingerprint(uri.getQuery()); - } else if ("imto".equals(scheme)) { + } else if ("imto".equalsIgnoreCase(scheme)) { // sample: imto://xmpp/jid@foo.com try { jid = URLDecoder.decode(uri.getEncodedPath(), "UTF-8").split("/")[1]; @@ -73,7 +73,7 @@ public class XmppUri { public Jid getJid() { try { - return this.jid == null ? null :Jid.fromString(this.jid); + return this.jid == null ? null :Jid.fromString(this.jid.toLowerCase()); } catch (InvalidJidException e) { return null; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 19e271b2..48dc2150 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -242,6 +242,13 @@ public class XmppConnection implements Runnable { @Override public void run() { + try { + if (socket != null) { + socket.close(); + } + } catch (final IOException ignored) { + + } connect(); } @@ -515,8 +522,9 @@ public class XmppConnection implements Runnable { sslSocket.setEnabledProtocols(supportProtocols); - final String[] cipherSuites = CryptoHelper.getSupportedCipherSuites( + final String[] cipherSuites = CryptoHelper.getOrderedCipherSuites( sslSocket.getSupportedCipherSuites()); + //Log.d(Config.LOGTAG, "Using ciphers: " + Arrays.toString(cipherSuites)); if (cipherSuites.length > 0) { sslSocket.setEnabledCipherSuites(cipherSuites); } @@ -658,6 +666,12 @@ public class XmppConnection implements Runnable { } private void sendBindRequest() { + while(!mXmppConnectionService.areMessagesInitialized()) { + try { + Thread.sleep(500); + } catch (final InterruptedException ignored) { + } + } final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") .addChild("resource").setContent(account.getResource()); @@ -673,28 +687,11 @@ public class XmppConnection implements Runnable { } catch (final InvalidJidException e) { // TODO: Handle the case where an external JID is technically invalid? } - if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { - smVersion = 3; - final EnablePacket enable = new EnablePacket(smVersion); - tagWriter.writeStanzaAsync(enable); - stanzasSent = 0; - messageReceipts.clear(); - } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { - smVersion = 2; - final EnablePacket enable = new EnablePacket(smVersion); - tagWriter.writeStanzaAsync(enable); - stanzasSent = 0; - messageReceipts.clear(); - } - features.carbonsEnabled = false; - features.blockListRequested = false; - disco.clear(); - sendServiceDiscoveryInfo(account.getServer()); - sendServiceDiscoveryItems(account.getServer()); - if (bindListener != null) { - bindListener.onBind(account); + if (streamFeatures.hasChild("session")) { + sendStartSession(); + } else { + sendPostBindInitialization(); } - sendInitialPing(); } else { disconnect(true); } @@ -703,12 +700,45 @@ public class XmppConnection implements Runnable { } } }); - if (this.streamFeatures.hasChild("session")) { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending deprecated session"); - final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); - startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); - this.sendUnmodifiedIqPacket(startSession, null); + } + + private void sendStartSession() { + final IqPacket startSession = new IqPacket(IqPacket.TYPE.SET); + startSession.addChild("session","urn:ietf:params:xml:ns:xmpp-session"); + this.sendUnmodifiedIqPacket(startSession, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.RESULT) { + sendPostBindInitialization(); + } else { + disconnect(true); + } + } + }); + } + + private void sendPostBindInitialization() { + smVersion = 0; + if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { + smVersion = 3; + } else if (streamFeatures.hasChild("sm", "urn:xmpp:sm:2")) { + smVersion = 2; + } + if (smVersion != 0) { + final EnablePacket enable = new EnablePacket(smVersion); + tagWriter.writeStanzaAsync(enable); + stanzasSent = 0; + messageReceipts.clear(); } + features.carbonsEnabled = false; + features.blockListRequested = false; + disco.clear(); + sendServiceDiscoveryInfo(account.getServer()); + sendServiceDiscoveryItems(account.getServer()); + if (bindListener != null) { + bindListener.onBind(account); + } + sendInitialPing(); } private void sendServiceDiscoveryInfo(final Jid server) { @@ -1027,6 +1057,11 @@ public class XmppConnection implements Runnable { this.sendPacket(new InactivePacket()); } + public void resetAttemptCount() { + this.attempt = 0; + this.lastConnect = 0; + } + public class Features { XmppConnection connection; private boolean carbonsEnabled = false; diff --git a/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java new file mode 100644 index 00000000..f85efbdb --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/chatstate/ChatState.java @@ -0,0 +1,32 @@ +package eu.siacs.conversations.xmpp.chatstate; + +import eu.siacs.conversations.xml.Element; + +public enum ChatState { + + ACTIVE, INACTIVE, GONE, COMPOSING, PAUSED, mIncomingChatState; + + public static ChatState parse(Element element) { + final String NAMESPACE = "http://jabber.org/protocol/chatstates"; + if (element.hasChild("active",NAMESPACE)) { + return ACTIVE; + } else if (element.hasChild("inactive",NAMESPACE)) { + return INACTIVE; + } else if (element.hasChild("composing",NAMESPACE)) { + return COMPOSING; + } else if (element.hasChild("gone",NAMESPACE)) { + return GONE; + } else if (element.hasChild("paused",NAMESPACE)) { + return PAUSED; + } else { + return null; + } + } + + public static Element toElement(ChatState state) { + final String NAMESPACE = "http://jabber.org/protocol/chatstates"; + final Element element = new Element(state.toString().toLowerCase()); + element.setAttribute("xmlns",NAMESPACE); + return element; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java index f1855263..164e8849 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jid/InvalidJidException.java @@ -8,6 +8,7 @@ public class InvalidJidException extends Exception { public final static String INVALID_PART_LENGTH = "JID part must be between 0 and 1023 characters"; public final static String INVALID_CHARACTER = "JID contains an invalid character"; public final static String STRINGPREP_FAIL = "The STRINGPREP operation has failed for the given JID"; + public final static String IS_NULL = "JID can not be NULL"; /** * Constructs a new {@code Exception} that includes the current stack trace. diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java index a35ea37c..295e067a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java @@ -1,9 +1,12 @@ package eu.siacs.conversations.xmpp.jid; +import android.util.LruCache; + import net.java.otr4j.session.SessionID; import java.net.IDN; +import eu.siacs.conversations.Config; import gnu.inet.encoding.Stringprep; import gnu.inet.encoding.StringprepException; @@ -12,6 +15,8 @@ import gnu.inet.encoding.StringprepException; */ public final class Jid { + private static LruCache<String,Jid> cache = new LruCache<>(1024); + private final String localpart; private final String domainpart; private final String resourcepart; @@ -41,7 +46,11 @@ public final class Jid { } public static Jid fromString(final String jid) throws InvalidJidException { - return new Jid(jid); + return Jid.fromString(jid, false); + } + + public static Jid fromString(final String jid, final boolean safe) throws InvalidJidException { + return new Jid(jid, safe); } public static Jid fromParts(final String localpart, @@ -56,10 +65,21 @@ public final class Jid { if (resourcepart != null && !resourcepart.isEmpty()) { out = out + "/" + resourcepart; } - return new Jid(out); + return new Jid(out, false); } - private Jid(final String jid) throws InvalidJidException { + private Jid(final String jid, final boolean safe) throws InvalidJidException { + if (jid == null) throw new InvalidJidException(InvalidJidException.IS_NULL); + + Jid fromCache = Jid.cache.get(jid); + if (fromCache != null) { + displayjid = fromCache.displayjid; + localpart = fromCache.localpart; + domainpart = fromCache.domainpart; + resourcepart = fromCache.resourcepart; + return; + } + // Hackish Android way to count the number of chars in a string... should work everywhere. final int atCount = jid.length() - jid.replace("@", "").length(); final int slashCount = jid.length() - jid.replace("/", "").length(); @@ -88,7 +108,7 @@ public final class Jid { } else { final String lp = jid.substring(0, atLoc); try { - localpart = Stringprep.nodeprep(lp); + localpart = Config.DISABLE_STRING_PREP || safe ? lp : Stringprep.nodeprep(lp); } catch (final StringprepException e) { throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); } @@ -103,7 +123,7 @@ public final class Jid { if (slashCount > 0) { final String rp = jid.substring(slashLoc + 1, jid.length()); try { - resourcepart = Stringprep.resourceprep(rp); + resourcepart = Config.DISABLE_STRING_PREP || safe ? rp : Stringprep.resourceprep(rp); } catch (final StringprepException e) { throw new InvalidJidException(InvalidJidException.STRINGPREP_FAIL, e); } @@ -139,6 +159,8 @@ public final class Jid { throw new InvalidJidException(InvalidJidException.INVALID_PART_LENGTH); } + Jid.cache.put(jid, this); + this.displayjid = finaljid; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java index 4e7b532b..93aaa68c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -26,7 +26,7 @@ public class MessagePacket extends AbstractStanza { this.children.remove(findChild("body")); Element body = new Element("body"); body.setContent(text); - this.children.add(body); + this.children.add(0, body); } public void setType(int type) { @@ -39,6 +39,9 @@ public class MessagePacket extends AbstractStanza { break; case TYPE_NORMAL: break; + case TYPE_ERROR: + this.setAttribute("type","error"); + break; default: this.setAttribute("type", "chat"); break; |