diff --git a/art/ic_send_picture_away.svg b/art/ic_send_picture_away.svg new file mode 100644 index 00000000..a85a1eec --- /dev/null +++ b/art/ic_send_picture_away.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/art/ic_send_picture_dnd.svg b/art/ic_send_picture_dnd.svg new file mode 100644 index 00000000..0c7d0635 --- /dev/null +++ b/art/ic_send_picture_dnd.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/art/ic_send_picture_offline.svg b/art/ic_send_picture_offline.svg new file mode 100644 index 00000000..048508a3 --- /dev/null +++ b/art/ic_send_picture_offline.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/art/ic_send_picture_online.svg b/art/ic_send_picture_online.svg new file mode 100644 index 00000000..06181bbd --- /dev/null +++ b/art/ic_send_picture_online.svg @@ -0,0 +1,55 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/art/render.rb b/art/render.rb index ad53b1f4..698abea5 100755 --- a/art/render.rb +++ b/art/render.rb @@ -30,6 +30,10 @@ images = { 'ic_send_cancel_offline.svg' => ['ic_send_cancel_offline', 36], 'ic_send_cancel_away.svg' => ['ic_send_cancel_away', 36], 'ic_send_cancel_dnd.svg' => ['ic_send_cancel_dnd', 36], + 'ic_send_picture_online.svg' => ['ic_send_picture_online', 36], + 'ic_send_picture_offline.svg' => ['ic_send_picture_offline', 36], + 'ic_send_picture_away.svg' => ['ic_send_picture_away', 36], + 'ic_send_picture_dnd.svg' => ['ic_send_picture_dnd', 36] } images.each do |source, result| resolutions.each do |name, factor| diff --git a/build.gradle b/build.gradle index b57b6c86..03451412 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:1.0.1' + classpath 'com.android.tools.build:gradle:1.2.3' } } @@ -35,6 +35,7 @@ dependencies { compile 'com.google.zxing:android-integration:3.1.0' compile 'de.measite.minidns:minidns:0.1.3' compile 'de.timroes.android:EnhancedListView:0.3.4' + compile 'me.leolin:ShortcutBadger:1.1.1@aar' } android { diff --git a/libs/MemorizingTrustManager/build.gradle b/libs/MemorizingTrustManager/build.gradle index aa022a93..47491fec 100644 --- a/libs/MemorizingTrustManager/build.gradle +++ b/libs/MemorizingTrustManager/build.gradle @@ -3,7 +3,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:0.7.+' + classpath 'com.android.tools.build:gradle:1.2.3' } } diff --git a/libs/openpgp-api-lib/build.gradle b/libs/openpgp-api-lib/build.gradle index 6e8fdfb5..a991cf26 100644 --- a/libs/openpgp-api-lib/build.gradle +++ b/libs/openpgp-api-lib/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:0.12.2' + classpath 'com.android.tools.build:gradle:1.2.3' } } diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/src/main/java/eu/siacs/conversations/crypto/OtrService.java similarity index 97% rename from src/main/java/eu/siacs/conversations/crypto/OtrEngine.java rename to src/main/java/eu/siacs/conversations/crypto/OtrService.java index 0dc7c37e..1e905bac 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/OtrService.java @@ -36,14 +36,14 @@ import net.java.otr4j.session.InstanceTag; import net.java.otr4j.session.SessionID; import net.java.otr4j.session.FragmenterInstructions; -public class OtrEngine extends OtrCryptoEngineImpl implements OtrEngineHost { +public class OtrService extends OtrCryptoEngineImpl implements OtrEngineHost { private Account account; private OtrPolicy otrPolicy; private KeyPair keyPair; private XmppConnectionService mXmppConnectionService; - public OtrEngine(XmppConnectionService service, Account account) { + public OtrService(XmppConnectionService service, Account account) { this.account = account; this.otrPolicy = new OtrPolicyImpl(); this.otrPolicy.setAllowV1(false); @@ -285,7 +285,7 @@ public class OtrEngine extends OtrCryptoEngineImpl implements OtrEngineHost { @Override public void verify(SessionID id, String fingerprint, boolean approved) { - Log.d(Config.LOGTAG,"OtrEngine.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")"); + Log.d(Config.LOGTAG,"OtrService.verify("+id.toString()+","+fingerprint+","+String.valueOf(approved)+")"); try { final Jid jid = Jid.fromSessionID(id); Conversation conversation = this.mXmppConnectionService.find(this.account,jid); diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index fe103094..6e6dcb6a 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -19,7 +19,7 @@ import java.util.concurrent.CopyOnWriteArraySet; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.OtrEngine; +import eu.siacs.conversations.crypto.OtrService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -117,7 +117,7 @@ public class Account extends AbstractEntity { protected JSONObject keys = new JSONObject(); protected String avatar; protected boolean online = false; - private OtrEngine otrEngine = null; + private OtrService mOtrService = null; private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private String otrFingerprint; @@ -273,12 +273,12 @@ public class Account extends AbstractEntity { return values; } - public void initOtrEngine(final XmppConnectionService context) { - this.otrEngine = new OtrEngine(context, this); + public void initAccountServices(final XmppConnectionService context) { + this.mOtrService = new OtrService(context, this); } - public OtrEngine getOtrEngine() { - return this.otrEngine; + public OtrService getOtrService() { + return this.mOtrService; } public XmppConnection getXmppConnection() { @@ -292,10 +292,10 @@ public class Account extends AbstractEntity { public String getOtrFingerprint() { if (this.otrFingerprint == null) { try { - if (this.otrEngine == null) { + if (this.mOtrService == null) { return null; } - final PublicKey publicKey = this.otrEngine.getPublicKey(); + final PublicKey publicKey = this.mOtrService.getPublicKey(); if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { return null; } diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index f81f1a87..cc6f146b 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -75,12 +75,7 @@ public class Bookmark extends Element implements ListItem { } public String getNick() { - Element nick = this.findChild("nick"); - if (nick != null) { - return nick.getContent(); - } else { - return null; - } + return this.findChildContent("nick"); } public void setNick(String nick) { @@ -96,12 +91,7 @@ public class Bookmark extends Element implements ListItem { } public String getPassword() { - Element password = this.findChild("password"); - if (password != null) { - return password.getContent(); - } else { - return null; - } + return this.findChildContent("password"); } public void setPassword(String password) { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 9dbca59a..e546f214 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -237,8 +237,16 @@ public class Contact implements ListItem, Blockable { return this.presences.getMostAvailableStatus(); } - public void setPhotoUri(String uri) { - this.photoUri = uri; + public boolean setPhotoUri(String uri) { + if (uri != null && !uri.equals(this.photoUri)) { + this.photoUri = uri; + return true; + } else if (this.photoUri != null && uri == null) { + this.photoUri = null; + return true; + } else { + return false; + } } public void setServerName(String serverName) { diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index 95a8c957..289ed4ea 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -249,6 +249,12 @@ public class Conversation extends AbstractEntity implements Blockable { this.mLastReceivedOtrMessageId = id; } + public int countMessages() { + synchronized (this.messages) { + return this.messages.size(); + } + } + public interface OnMessageFound { public void onMessageFound(final Message message); @@ -419,7 +425,7 @@ public class Conversation extends AbstractEntity implements Blockable { final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(), presence, "xmpp"); - this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine()); + this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService()); try { if (sendStart) { this.otrSession.startSession(); @@ -491,7 +497,7 @@ public class Conversation extends AbstractEntity implements Blockable { return null; } DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); - this.otrFingerprint = getAccount().getOtrEngine().getFingerprint(remotePubKey); + this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey); } catch (final OtrCryptoException | UnsupportedOperationException ignored) { return null; } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 38152edb..a63d033d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -17,7 +17,7 @@ public class Message extends AbstractEntity { public static final String TABLENAME = "messages"; - public static final String MERGE_SEPARATOR = "\u200B\n\n"; + public static final String MERGE_SEPARATOR = " \u200B\n\n"; public static final int STATUS_RECEIVED = 0; public static final int STATUS_UNSEND = 1; @@ -328,7 +328,7 @@ public class Message extends AbstractEntity { return this.remoteMsgId == null && this.counterpart.equals(message.getCounterpart()) && this.body.equals(message.getBody()) - && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.PING_TIMEOUT * 500; + && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000; } } diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java index ce058004..d6777ef6 100644 --- a/src/main/java/eu/siacs/conversations/entities/Roster.java +++ b/src/main/java/eu/siacs/conversations/entities/Roster.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.entities; import java.util.ArrayList; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import eu.siacs.conversations.xmpp.jid.Jid; @@ -55,12 +56,15 @@ public class Roster { } } - public void clearSystemAccounts() { - for (Contact contact : getContacts()) { - contact.setPhotoUri(null); - contact.setSystemName(null); - contact.setSystemAccount(null); + public List getWithSystemAccounts() { + List with = getContacts(); + for(Iterator iterator = with.iterator(); iterator.hasNext();) { + Contact contact = iterator.next(); + if (contact.getSystemAccount() == null) { + iterator.remove(); + } } + return with; } public List getContacts() { diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index bfe84440..24e93db1 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -11,6 +11,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.MessagePacket; public abstract class AbstractParser { @@ -20,22 +21,23 @@ public abstract class AbstractParser { this.mXmppConnectionService = service; } + public static Long getTimestamp(Element element, Long defaultValue) { + Element delay = element.findChild("delay","urn:xmpp:delay"); + if (delay != null) { + String stamp = delay.getAttribute("stamp"); + if (stamp != null) { + try { + return AbstractParser.parseTimestamp(delay.getAttribute("stamp")).getTime(); + } catch (ParseException e) { + return defaultValue; + } + } + } + return defaultValue; + } + protected long getTimestamp(Element packet) { - long now = System.currentTimeMillis(); - Element delay = packet.findChild("delay"); - if (delay == null) { - return now; - } - String stamp = delay.getAttribute("stamp"); - if (stamp == null) { - return now; - } - try { - long time = parseTimestamp(stamp).getTime(); - return now < time ? now : time; - } catch (ParseException e) { - return now; - } + return getTimestamp(packet,System.currentTimeMillis()); } public static Date parseTimestamp(String timestamp) throws ParseException { @@ -46,14 +48,11 @@ public abstract class AbstractParser { return dateFormat.parse(timestamp); } - 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 boolean presenceOverwrite) { + updateLastseen(packet, account, packet.getAttributeAsJid("from"), presenceOverwrite); } - protected void updateLastseen(final Element packet, final Account account, final Jid from, - final boolean 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); @@ -70,10 +69,6 @@ public abstract class AbstractParser { if (item == null) { return null; } - Element data = item.findChild("data", "urn:xmpp:avatar:data"); - if (data == null) { - return null; - } - return data.getContent(); + return item.findChildContent("data", "urn:xmpp:avatar:data"); } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 7870fdbf..96568fbd 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -1,8 +1,12 @@ package eu.siacs.conversations.parser; +import android.util.Log; +import android.util.Pair; + import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; +import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; @@ -25,12 +29,12 @@ public class MessageParser extends AbstractParser implements super(service); } - private boolean extractChatState(Conversation conversation, final Element element) { - ChatState state = ChatState.parse(element); + private boolean extractChatState(Conversation conversation, final MessagePacket packet) { + ChatState state = ChatState.parse(packet); if (state != null && conversation != null) { final Account account = conversation.getAccount(); - Jid from = element.getAttributeAsJid("from"); - if (from != null && from.toBareJid().equals(account.getJid().toBareJid())) { + Jid from = packet.getFrom(); + if (from.toBareJid().equals(account.getJid().toBareJid())) { conversation.setOutgoingChatState(state); return false; } else { @@ -40,86 +44,27 @@ public class MessageParser extends AbstractParser implements return false; } - private Message parseChat(MessagePacket packet, Account account) { - final Jid jid = packet.getFrom(); - if (jid == null) { - return null; - } - Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid.toBareJid(), false); - String pgpBody = getPgpBody(packet); - Message finishedMessage; - if (pgpBody != null) { - finishedMessage = new Message(conversation, - pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECEIVED); - } else { - finishedMessage = new Message(conversation, - packet.getBody(), Message.ENCRYPTION_NONE, - Message.STATUS_RECEIVED); - } - finishedMessage.setRemoteMsgId(packet.getId()); - 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(trueCounterpart); - if (conversation.hasDuplicateMessage(finishedMessage)) { - return null; - } - } else { - updateLastseen(packet, account, true); - } - finishedMessage.setCounterpart(jid); - finishedMessage.setTime(getTimestamp(packet)); - extractChatState(conversation,packet); - return finishedMessage; - } - - private Message parseOtrChat(MessagePacket packet, Account account) { - final Jid to = packet.getTo(); - final Jid from = packet.getFrom(); - if (to == null || from == null) { - return null; - } - boolean properlyAddressed = !to.isBareJid() || account.countPresences() == 1; - Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, from.toBareJid(), false); + private Message parseOtrChat(String body, Jid from, String id, Conversation conversation) { String presence; if (from.isBareJid()) { presence = ""; } else { presence = from.getResourcepart(); } - extractChatState(conversation, packet); - updateLastseen(packet, account, true); - String body = packet.getBody(); if (body.matches("^\\?OTRv\\d{1,2}\\?.*")) { conversation.endOtrIfNeeded(); } if (!conversation.hasValidOtrSession()) { - if (properlyAddressed) { - conversation.startOtrSession(presence,false); - } else { - return null; - } + conversation.startOtrSession(presence,false); } else { - String foreignPresence = conversation.getOtrSession() - .getSessionID().getUserID(); + String foreignPresence = conversation.getOtrSession().getSessionID().getUserID(); if (!foreignPresence.equals(presence)) { conversation.endOtrIfNeeded(); - if (properlyAddressed) { - conversation.startOtrSession(presence, false); - } else { - return null; - } + conversation.startOtrSession(presence, false); } } try { - conversation.setLastReceivedOtrMessageId(packet.getId()); + conversation.setLastReceivedOtrMessageId(id); Session otrSession = conversation.getOtrSession(); SessionStatus before = otrSession.getSessionStatus(); body = otrSession.transformReceiving(body); @@ -140,12 +85,7 @@ public class MessageParser extends AbstractParser implements conversation.setSymmetricKey(CryptoHelper.hexToBytes(key)); return null; } - Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, - Message.STATUS_RECEIVED); - finishedMessage.setTime(getTimestamp(packet)); - finishedMessage.setRemoteMsgId(packet.getId()); - finishedMessage.markable = isMarkable(packet); - finishedMessage.setCounterpart(from); + Message finishedMessage = new Message(conversation, body, Message.ENCRYPTION_OTR, Message.STATUS_RECEIVED); conversation.setLastReceivedOtrMessageId(null); return finishedMessage; } catch (Exception e) { @@ -154,293 +94,6 @@ public class MessageParser extends AbstractParser implements } } - private Message parseGroupchat(MessagePacket packet, Account account) { - int status; - final Jid from = packet.getFrom(); - if (from == null) { - return null; - } - if (mXmppConnectionService.find(account.pendingConferenceLeaves, - account, from.toBareJid()) != null) { - return null; - } - 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()); - mXmppConnectionService.updateConversationUi(); - return null; - } - - final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user"); - if (from.isBareJid() && (x == null || !x.hasChild("status"))) { - return null; - } else if (from.isBareJid() && x.hasChild("status")) { - for(Element child : x.getChildren()) { - if (child.getName().equals("status")) { - String code = child.getAttribute("code"); - if (code.contains(MucOptions.STATUS_CODE_ROOM_CONFIG_CHANGED)) { - mXmppConnectionService.fetchConferenceConfiguration(conversation); - } - } - } - return null; - } - - if (from.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { - if (mXmppConnectionService.markMessage(conversation, - packet.getId(), Message.STATUS_SEND_RECEIVED)) { - return null; - } else if (packet.getId() == null) { - Message message = conversation.findSentMessageWithBody(packet.getBody()); - if (message != null) { - mXmppConnectionService.markMessage(message,Message.STATUS_SEND_RECEIVED); - return null; - } else { - status = Message.STATUS_SEND; - } - } else { - status = Message.STATUS_SEND; - } - } else { - status = Message.STATUS_RECEIVED; - } - String pgpBody = getPgpBody(packet); - Message finishedMessage; - if (pgpBody == null) { - finishedMessage = new Message(conversation, - packet.getBody(), Message.ENCRYPTION_NONE, status); - } else { - finishedMessage = new Message(conversation, pgpBody, - Message.ENCRYPTION_PGP, status); - } - finishedMessage.setRemoteMsgId(packet.getId()); - finishedMessage.markable = isMarkable(packet); - finishedMessage.setCounterpart(from); - if (status == Message.STATUS_RECEIVED) { - finishedMessage.setTrueCounterpart(conversation.getMucOptions() - .getTrueCounterpart(from.getResourcepart())); - } - if (packet.hasChild("delay") - && conversation.hasDuplicateMessage(finishedMessage)) { - return null; - } - finishedMessage.setTime(getTimestamp(packet)); - return finishedMessage; - } - - private Message parseCarbonMessage(final MessagePacket packet, final Account account) { - int status; - final Jid fullJid; - Element forwarded; - if (packet.hasChild("received", "urn:xmpp:carbons:2")) { - forwarded = packet.findChild("received", "urn:xmpp:carbons:2") - .findChild("forwarded", "urn:xmpp:forward:0"); - status = Message.STATUS_RECEIVED; - } else if (packet.hasChild("sent", "urn:xmpp:carbons:2")) { - forwarded = packet.findChild("sent", "urn:xmpp:carbons:2") - .findChild("forwarded", "urn:xmpp:forward:0"); - status = Message.STATUS_SEND; - } else { - return null; - } - if (forwarded == null) { - return null; - } - Element message = forwarded.findChild("message"); - if (message == null) { - return null; - } - if (!message.hasChild("body")) { - if (status == Message.STATUS_RECEIVED - && message.getAttribute("from") != null) { - parseNonMessage(message, account); - } else if (status == Message.STATUS_SEND - && message.hasChild("displayed", "urn:xmpp:chat-markers:0")) { - final Jid to = message.getAttributeAsJid("to"); - if (to != null) { - final Conversation conversation = mXmppConnectionService.find( - mXmppConnectionService.getConversations(), account, - to.toBareJid()); - if (conversation != null) { - mXmppConnectionService.markRead(conversation); - } - } - } - return null; - } - if (status == Message.STATUS_RECEIVED) { - fullJid = message.getAttributeAsJid("from"); - if (fullJid == null) { - return null; - } else { - updateLastseen(message, account, true); - } - } else { - fullJid = message.getAttributeAsJid("to"); - if (fullJid == null) { - return null; - } - } - if (message.hasChild("x","http://jabber.org/protocol/muc#user") - && "chat".equals(message.getAttribute("type"))) { - return null; - } - Conversation conversation = mXmppConnectionService - .findOrCreateConversation(account, fullJid.toBareJid(), false); - String pgpBody = getPgpBody(message); - Message finishedMessage; - if (pgpBody != null) { - finishedMessage = new Message(conversation, pgpBody, - Message.ENCRYPTION_PGP, status); - } else { - String body = message.findChild("body").getContent(); - 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); - finishedMessage.setCounterpart(fullJid); - if (conversation.getMode() == Conversation.MODE_MULTI - && !fullJid.isBareJid()) { - finishedMessage.setType(Message.TYPE_PRIVATE); - finishedMessage.setTrueCounterpart(conversation.getMucOptions() - .getTrueCounterpart(fullJid.getResourcepart())); - if (conversation.hasDuplicateMessage(finishedMessage)) { - return null; - } - } - return finishedMessage; - } - - private Message parseMamMessage(MessagePacket packet, final Account account) { - final Element result = packet.findChild("result","urn:xmpp:mam:0"); - if (result == null ) { - return null; - } - final MessageArchiveService.Query query = this.mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid")); - if (query!=null) { - query.incrementTotalCount(); - } - final Element forwarded = result.findChild("forwarded","urn:xmpp:forward:0"); - if (forwarded == null) { - return null; - } - final Element message = forwarded.findChild("message"); - if (message == null) { - return null; - } - final Element body = message.findChild("body"); - if (body == null || message.hasChild("private","urn:xmpp:carbons:2") || message.hasChild("no-copy","urn:xmpp:hints")) { - return null; - } - int encryption; - String content = getPgpBody(message); - if (content != null) { - encryption = Message.ENCRYPTION_PGP; - } else { - encryption = Message.ENCRYPTION_NONE; - content = body.getContent(); - } - if (content == null) { - return null; - } - final long timestamp = getTimestamp(forwarded); - final Jid to = message.getAttributeAsJid("to"); - final Jid from = message.getAttributeAsJid("from"); - Jid counterpart; - int status; - Conversation conversation; - if (from!=null && to != null && from.toBareJid().equals(account.getJid().toBareJid())) { - status = Message.STATUS_SEND; - conversation = this.mXmppConnectionService.findOrCreateConversation(account,to.toBareJid(),false,query); - counterpart = to; - } else if (from !=null && to != null) { - status = Message.STATUS_RECEIVED; - conversation = this.mXmppConnectionService.findOrCreateConversation(account,from.toBareJid(),false,query); - counterpart = from; - } else { - return null; - } - Message finishedMessage = new Message(conversation,content,encryption,status); - finishedMessage.setTime(timestamp); - finishedMessage.setCounterpart(counterpart); - finishedMessage.setRemoteMsgId(message.getAttribute("id")); - finishedMessage.setServerMsgId(result.getAttribute("id")); - if (conversation.hasDuplicateMessage(finishedMessage)) { - return null; - } - if (query!=null) { - query.incrementMessageCount(); - } - return finishedMessage; - } - - private void parseError(final MessagePacket packet, final Account account) { - final Jid from = packet.getFrom(); - mXmppConnectionService.markMessage(account, from.toBareJid(), - packet.getId(), Message.STATUS_SEND_FAILED); - } - - private void parseNonMessage(Element packet, Account account) { - final Jid from = packet.getAttributeAsJid("from"); - if (account.getJid().equals(from)) { - return; - } - if (extractChatState(from == null ? null : mXmppConnectionService.find(account,from), packet)) { - mXmppConnectionService.updateConversationUi(); - } - Invite invite = extractInvite(packet); - if (invite != null && invite.jid != null) { - Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, invite.jid, true); - if (!conversation.getMucOptions().online()) { - conversation.getMucOptions().setPassword(invite.password); - mXmppConnectionService.databaseBackend.updateConversation(conversation); - mXmppConnectionService.joinMuc(conversation); - mXmppConnectionService.updateConversationUi(); - } - } - if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { - 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")) { - String id = packet - .findChild("displayed", "urn:xmpp:chat-markers:0") - .getAttribute("id"); - updateLastseen(packet, account, true); - 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") - .getAttribute("id"); - updateLastseen(packet, account, false); - mXmppConnectionService.markMessage(account, from.toBareJid(), - id, Message.STATUS_SEND_RECEIVED); - } else if (from != null - && packet.hasChild("received", "urn:xmpp:receipts")) { - String id = packet.findChild("received", "urn:xmpp:receipts") - .getAttribute("id"); - updateLastseen(packet, account, false); - mXmppConnectionService.markMessage(account, from.toBareJid(), - id, Message.STATUS_SEND_RECEIVED); - } - } - private class Invite { Jid jid; String password; @@ -448,10 +101,24 @@ public class MessageParser extends AbstractParser implements this.jid = jid; this.password = password; } + + public boolean execute(Account account) { + if (jid != null) { + Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, jid, true); + if (!conversation.getMucOptions().online()) { + conversation.getMucOptions().setPassword(password); + mXmppConnectionService.databaseBackend.updateConversation(conversation); + mXmppConnectionService.joinMuc(conversation); + mXmppConnectionService.updateConversationUi(); + } + return true; + } + return false; + } } private Invite extractInvite(Element message) { - Element x = message.findChild("x","http://jabber.org/protocol/muc#user"); + Element x = message.findChild("x", "http://jabber.org/protocol/muc#user"); if (x != null) { Element invite = x.findChild("invite"); if (invite != null) { @@ -469,34 +136,23 @@ public class MessageParser extends AbstractParser implements private void parseEvent(final Element event, final Jid from, final Account account) { Element items = event.findChild("items"); - if (items == null) { - return; - } - String node = items.getAttribute("node"); - if (node == null) { - return; - } - if (node.equals("urn:xmpp:avatar:metadata")) { + String node = items == null ? null : items.getAttribute("node"); + if ("urn:xmpp:avatar:metadata".equals(node)) { Avatar avatar = Avatar.parseMetadata(items); if (avatar != null) { avatar.owner = from; - if (mXmppConnectionService.getFileBackend().isAvatarCached( - avatar)) { + if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { if (account.getJid().toBareJid().equals(from)) { if (account.setAvatar(avatar.getFilename())) { - mXmppConnectionService.databaseBackend - .updateAccount(account); + mXmppConnectionService.databaseBackend.updateAccount(account); } - mXmppConnectionService.getAvatarService().clear( - account); + mXmppConnectionService.getAvatarService().clear(account); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateAccountUi(); } else { - Contact contact = account.getRoster().getContact( - from); + Contact contact = account.getRoster().getContact(from); contact.setAvatar(avatar); - mXmppConnectionService.getAvatarService().clear( - contact); + mXmppConnectionService.getAvatarService().clear(contact); mXmppConnectionService.updateConversationUi(); mXmppConnectionService.updateRosterUi(); } @@ -504,161 +160,257 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.fetchAvatar(account, avatar); } } - } else if (node.equals("http://jabber.org/protocol/nick")) { - Element item = items.findChild("item"); - if (item != null) { - Element nick = item.findChild("nick", - "http://jabber.org/protocol/nick"); - if (nick != null) { - if (from != null) { - Contact contact = account.getRoster().getContact( - from); - contact.setPresenceName(nick.getContent()); - mXmppConnectionService.getAvatarService().clear(account); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateAccountUi(); - } - } + } else if ("http://jabber.org/protocol/nick".equals(node)) { + Element i = items.findChild("item"); + Element nick = i == null ? null : i.findChild("nick", "http://jabber.org/protocol/nick"); + if (nick != null) { + Contact contact = account.getRoster().getContact(from); + contact.setPresenceName(nick.getContent()); + mXmppConnectionService.getAvatarService().clear(account); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateAccountUi(); } } } - private String getPgpBody(Element message) { - Element child = message.findChild("x", "jabber:x:encrypted"); - if (child == null) { - return null; - } else { - return child.getContent(); + private boolean handleErrorMessage(Account account, MessagePacket packet) { + if (packet.getType() == MessagePacket.TYPE_ERROR) { + Jid from = packet.getFrom(); + if (from != null) { + mXmppConnectionService.markMessage(account, from.toBareJid(), packet.getId(), Message.STATUS_SEND_FAILED); + } + return true; } - } - - private boolean isMarkable(Element message) { - return message.hasChild("markable", "urn:xmpp:chat-markers:0"); + return false; } @Override - public void onMessagePacketReceived(Account account, MessagePacket packet) { - Message message = null; - this.parseNick(packet, account); - if ((packet.getType() == MessagePacket.TYPE_CHAT || packet.getType() == MessagePacket.TYPE_NORMAL)) { - if ((packet.getBody() != null) - && (packet.getBody().startsWith("?OTR"))) { - message = this.parseOtrChat(packet, account); - if (message != null) { - message.markUnread(); - } - } else if (packet.hasChild("body") && extractInvite(packet) == null) { - message = this.parseChat(packet, account); - if (message != null) { - message.markUnread(); - } - } else if (packet.hasChild("received", "urn:xmpp:carbons:2") - || (packet.hasChild("sent", "urn:xmpp:carbons:2"))) { - message = this.parseCarbonMessage(packet, account); - if (message != null) { - if (message.getStatus() == Message.STATUS_SEND) { - account.activateGracePeriod(); - mXmppConnectionService.markRead(message.getConversation()); - } else { - message.markUnread(); - } - } - } else if (packet.hasChild("result","urn:xmpp:mam:0")) { - message = parseMamMessage(packet, account); - if (message != null) { - Conversation conversation = message.getConversation(); - conversation.add(message); - mXmppConnectionService.databaseBackend.createMessage(message); - } + public void onMessagePacketReceived(Account account, MessagePacket original) { + if (handleErrorMessage(account, original)) { + return; + } + final MessagePacket packet; + Long timestamp = null; + final boolean isForwarded; + String serverMsgId = null; + final Element fin = original.findChild("fin", "urn:xmpp:mam:0"); + if (fin != null) { + mXmppConnectionService.getMessageArchiveService().processFin(fin,original.getFrom()); + return; + } + final Element result = original.findChild("result","urn:xmpp:mam:0"); + final MessageArchiveService.Query query = result == null ? null : mXmppConnectionService.getMessageArchiveService().findQuery(result.getAttribute("queryid")); + if (query != null && query.validFrom(original.getFrom())) { + Pair f = original.getForwardedMessagePacket("result", "urn:xmpp:mam:0"); + if (f == null) { return; - } else if (packet.hasChild("fin","urn:xmpp:mam:0")) { - Element fin = packet.findChild("fin","urn:xmpp:mam:0"); - mXmppConnectionService.getMessageArchiveService().processFin(fin); - } else { - parseNonMessage(packet, account); } - } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) { - message = this.parseGroupchat(packet, account); - if (message != null) { - if (message.getStatus() == Message.STATUS_RECEIVED) { - message.markUnread(); + timestamp = f.second; + packet = f.first; + isForwarded = true; + serverMsgId = result.getAttribute("id"); + query.incrementTotalCount(); + } else if (query != null) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender"); + return; + } else if (original.fromServer(account)) { + Pair f; + f = original.getForwardedMessagePacket("received", "urn:xmpp:carbons:2"); + f = f == null ? original.getForwardedMessagePacket("sent", "urn:xmpp:carbons:2") : f; + packet = f != null ? f.first : original; + if (handleErrorMessage(account, packet)) { + return; + } + timestamp = f != null ? f.second : null; + isForwarded = f != null; + } else { + packet = original; + isForwarded = false; + } + + if (timestamp == null) { + timestamp = AbstractParser.getTimestamp(packet, System.currentTimeMillis()); + } + final String body = packet.getBody(); + final String encrypted = packet.findChildContent("x", "jabber:x:encrypted"); + int status; + final Jid counterpart; + final Jid to = packet.getTo(); + final Jid from = packet.getFrom(); + final String remoteMsgId = packet.getId(); + boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT; + boolean properlyAddressed = !to.isBareJid() || account.countPresences() == 1; + if (packet.fromAccount(account)) { + status = Message.STATUS_SEND; + counterpart = to; + } else { + status = Message.STATUS_RECEIVED; + counterpart = from; + } + + if (from == null || to == null) { + Log.d(Config.LOGTAG,"no to or from in: "+packet.toString()); + return; + } + + Invite invite = extractInvite(packet); + if (invite != null && invite.execute(account)) { + return; + } + + if (extractChatState(mXmppConnectionService.find(account,from), packet)) { + mXmppConnectionService.updateConversationUi(); + } + + if (body != null || encrypted != null) { + Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.toBareJid(), isTypeGroupChat); + if (isTypeGroupChat) { + if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { + status = Message.STATUS_SEND_RECEIVED; + if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status)) { + return; + } else { + Message message = conversation.findSentMessageWithBody(body); + if (message != null) { + message.setRemoteMsgId(remoteMsgId); + mXmppConnectionService.markMessage(message, status); + return; + } + } } else { - mXmppConnectionService.markRead(message.getConversation()); - account.activateGracePeriod(); + status = Message.STATUS_RECEIVED; } } - } else if (packet.getType() == MessagePacket.TYPE_ERROR) { - this.parseError(packet, account); - return; - } else if (packet.getType() == MessagePacket.TYPE_HEADLINE) { - this.parseHeadline(packet, account); - return; - } - if ((message == null) || (message.getBody() == null)) { - return; - } - if ((mXmppConnectionService.confirmMessages()) - && ((packet.getId() != null))) { - if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { - MessagePacket receipt = mXmppConnectionService - .getMessageGenerator().received(account, packet, - "urn:xmpp:chat-markers:0"); - mXmppConnectionService.sendMessagePacket(account, receipt); + Message message; + if (body != null && body.startsWith("?OTR")) { + if (!isForwarded && !isTypeGroupChat && properlyAddressed) { + message = parseOtrChat(body, from, remoteMsgId, conversation); + if (message == null) { + return; + } + } else { + message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); + } + } else if (encrypted != null) { + message = new Message(conversation, encrypted, Message.ENCRYPTION_PGP, status); + } else { + message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } - if (packet.hasChild("request", "urn:xmpp:receipts")) { - MessagePacket receipt = mXmppConnectionService - .getMessageGenerator().received(account, packet, - "urn:xmpp:receipts"); - mXmppConnectionService.sendMessagePacket(account, receipt); + message.setCounterpart(counterpart); + message.setRemoteMsgId(remoteMsgId); + message.setServerMsgId(serverMsgId); + message.setTime(timestamp); + message.markable = packet.hasChild("markable", "urn:xmpp:chat-markers:0"); + if (conversation.getMode() == Conversation.MODE_MULTI) { + message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(counterpart.getResourcepart())); + if (!isTypeGroupChat) { + message.setType(Message.TYPE_PRIVATE); + } } - } - Conversation conversation = message.getConversation(); - conversation.add(message); - if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().advancedStreamFeaturesLoaded()) { - if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) { - mXmppConnectionService.updateConversation(conversation); + updateLastseen(packet,account,true); + boolean checkForDuplicates = serverMsgId != null + || (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay")) + || message.getType() == Message.TYPE_PRIVATE; + if (checkForDuplicates && conversation.hasDuplicateMessage(message)) { + Log.d(Config.LOGTAG,"skipping duplicate message from "+message.getCounterpart().toString()+" "+message.getBody()); + return; + } + if (query != null) { + query.incrementMessageCount(); + } + conversation.add(message); + if (serverMsgId == null) { + if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) { + mXmppConnectionService.markRead(conversation); + account.activateGracePeriod(); + } else { + message.markUnread(); + } + mXmppConnectionService.updateConversationUi(); } - } - if (message.getStatus() == Message.STATUS_RECEIVED - && conversation.getOtrSession() != null - && !conversation.getOtrSession().getSessionID().getUserID() - .equals(message.getCounterpart().getResourcepart())) { - conversation.endOtrIfNeeded(); - } + if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded) { + if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, "urn:xmpp:chat-markers:0"); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + if (packet.hasChild("request", "urn:xmpp:receipts")) { + MessagePacket receipt = mXmppConnectionService + .getMessageGenerator().received(account, packet, "urn:xmpp:receipts"); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + } + if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().advancedStreamFeaturesLoaded()) { + if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) { + mXmppConnectionService.updateConversation(conversation); + } + } - if (packet.getType() != MessagePacket.TYPE_ERROR) { - if (message.getEncryption() == Message.ENCRYPTION_NONE - || mXmppConnectionService.saveEncryptedMessages()) { + if (message.getStatus() == Message.STATUS_RECEIVED + && conversation.getOtrSession() != null + && !conversation.getOtrSession().getSessionID().getUserID() + .equals(message.getCounterpart().getResourcepart())) { + conversation.endOtrIfNeeded(); + } + + if (message.getEncryption() == Message.ENCRYPTION_NONE || mXmppConnectionService.saveEncryptedMessages()) { mXmppConnectionService.databaseBackend.createMessage(message); } - } - final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); - if (message.trusted() && message.bodyContainsDownloadable() && manager.getAutoAcceptFileSize() > 0) { - manager.createNewConnection(message); - } else if (!message.isRead()) { - mXmppConnectionService.getNotificationService().push(message); - } - mXmppConnectionService.updateConversationUi(); - } - - private void parseHeadline(MessagePacket packet, Account account) { - if (packet.hasChild("event", "http://jabber.org/protocol/pubsub#event")) { - Element event = packet.findChild("event", - "http://jabber.org/protocol/pubsub#event"); - parseEvent(event, packet.getFrom(), account); - } - } - - private void parseNick(MessagePacket packet, Account account) { - Element nick = packet.findChild("nick", - "http://jabber.org/protocol/nick"); - if (nick != null) { - if (packet.getFrom() != null) { - Contact contact = account.getRoster().getContact( - packet.getFrom()); - contact.setPresenceName(nick.getContent()); + final HttpConnectionManager manager = this.mXmppConnectionService.getHttpConnectionManager(); + if (message.trusted() && message.bodyContainsDownloadable() && manager.getAutoAcceptFileSize() > 0) { + manager.createNewConnection(message); + } else if (!message.isRead()) { + mXmppConnectionService.getNotificationService().push(message); + } + } else { //no body + if (packet.hasChild("subject") && isTypeGroupChat) { + Conversation conversation = mXmppConnectionService.find(account, from.toBareJid()); + if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setHasMessagesLeftOnServer(conversation.countMessages() > 0); + conversation.getMucOptions().setSubject(packet.findChildContent("subject")); + mXmppConnectionService.updateConversationUi(); + return; + } } } + + Element received = packet.findChild("received", "urn:xmpp:chat-markers:0"); + if (received == null) { + received = packet.findChild("received", "urn:xmpp:receipts"); + } + if (received != null && !packet.fromAccount(account)) { + mXmppConnectionService.markMessage(account, from.toBareJid(), received.getAttribute("id"), Message.STATUS_SEND_RECEIVED); + } + Element displayed = packet.findChild("displayed", "urn:xmpp:chat-markers:0"); + if (displayed != null) { + if (packet.fromAccount(account)) { + Conversation conversation = mXmppConnectionService.find(account,counterpart.toBareJid()); + if (conversation != null) { + mXmppConnectionService.markRead(conversation); + } + } else { + updateLastseen(packet, account, true); + final Message displayedMessage = mXmppConnectionService.markMessage(account, from.toBareJid(), displayed.getAttribute("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(); + } + } + } + + Element event = packet.findChild("event", "http://jabber.org/protocol/pubsub#event"); + if (event != null) { + parseEvent(event, from, account); + } + + String nick = packet.findChildContent("nick", "http://jabber.org/protocol/nick"); + if (nick != null) { + Contact contact = account.getRoster().getContact(from); + contact.setPresenceName(nick); + } } -} +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index f7872210..d83347d8 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -45,38 +45,37 @@ public class PresenceParser extends AbstractParser implements } public void parseContactPresence(PresencePacket packet, Account account) { - PresenceGenerator mPresenceGenerator = mXmppConnectionService - .getPresenceGenerator(); - if (packet.getFrom() == null) { + PresenceGenerator mPresenceGenerator = mXmppConnectionService.getPresenceGenerator(); + final Jid from = packet.getFrom(); + if (from == null) { return; } - final Jid from = packet.getFrom(); - String type = packet.getAttribute("type"); - Contact contact = account.getRoster().getContact(packet.getFrom()); + final String type = packet.getAttribute("type"); + final Contact contact = account.getRoster().getContact(from); if (type == null) { - String presence; - if (!from.isBareJid()) { - presence = from.getResourcepart(); - } else { - presence = ""; + String presence = from.isBareJid() ? "" : from.getResourcepart(); + contact.setPresenceName(packet.findChildContent("nick", "http://jabber.org/protocol/nick")); + Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); + if (avatar != null && !contact.isSelf()) { + avatar.owner = from.toBareJid(); + if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { + if (contact.setAvatar(avatar)) { + mXmppConnectionService.getAvatarService().clear(contact); + mXmppConnectionService.updateConversationUi(); + mXmppConnectionService.updateRosterUi(); + } + } else { + mXmppConnectionService.fetchAvatar(account, avatar); + } } int sizeBefore = contact.getPresences().size(); - contact.updatePresence(presence, - Presences.parseShow(packet.findChild("show"))); + contact.updatePresence(presence, Presences.parseShow(packet.findChild("show"))); PgpEngine pgp = mXmppConnectionService.getPgpEngine(); - if (pgp != null) { - Element x = packet.findChild("x", "jabber:x:signed"); - if (x != null) { - Element status = packet.findChild("status"); - String msg; - if (status != null) { - msg = status.getContent(); - } else { - msg = ""; - } - contact.setPgpKeyId(pgp.fetchKeyId(account, msg, - x.getContent())); - } + Element x = packet.findChild("x", "jabber:x:signed"); + if (pgp != null && x != null) { + Element status = packet.findChild("status"); + String msg = status != null ? status.getContent() : ""; + contact.setPgpKeyId(pgp.fetchKeyId(account, msg, x.getContent())); } boolean online = sizeBefore < contact.getPresences().size(); updateLastseen(packet, account, false); @@ -87,8 +86,7 @@ public class PresenceParser extends AbstractParser implements } else { contact.removePresence(from.getResourcepart()); } - mXmppConnectionService.onContactStatusChanged - .onContactStatusChanged(contact, false); + mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, false); } else if (type.equals("subscribe")) { if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { mXmppConnectionService.sendPresencePacket(account, @@ -97,25 +95,6 @@ public class PresenceParser extends AbstractParser implements contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); } } - Element nick = packet.findChild("nick", - "http://jabber.org/protocol/nick"); - if (nick != null) { - contact.setPresenceName(nick.getContent()); - } - Element x = packet.findChild("x","vcard-temp:x:update"); - Avatar avatar = Avatar.parsePresence(x); - if (avatar != null && !contact.isSelf()) { - avatar.owner = from.toBareJid(); - if (mXmppConnectionService.getFileBackend().isAvatarCached(avatar)) { - if (contact.setAvatar(avatar)) { - mXmppConnectionService.getAvatarService().clear(contact); - mXmppConnectionService.updateConversationUi(); - mXmppConnectionService.updateRosterUi(); - } - } else { - mXmppConnectionService.fetchAvatar(account,avatar); - } - } mXmppConnectionService.updateRosterUi(); } diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index f97077c4..0bc428c8 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -166,12 +166,12 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - public void processFin(Element fin) { + public void processFin(Element fin, Jid from) { if (fin == null) { return; } Query query = findQuery(fin.getAttribute("queryid")); - if (query == null) { + if (query == null || !query.validFrom(from)) { return; } boolean complete = fin.getAttributeAsBoolean("complete"); @@ -336,6 +336,14 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return this.messageCount; } + public boolean validFrom(Jid from) { + if (muc()) { + return getWith().equals(from); + } else { + return (from == null) || account.getJid().toBareJid().equals(from.toBareJid()); + } + } + @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index e111da95..49543eeb 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -134,6 +134,7 @@ public class NotificationService { } public void push(final Message message) { + mXmppConnectionService.updateUnreadCountBadge(); if (!notify(message)) { return; } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 97c52156..0a264dd1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -77,6 +77,7 @@ import eu.siacs.conversations.utils.ExceptionHelper; import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener; import eu.siacs.conversations.utils.PRNGFixes; import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnBindListener; @@ -100,6 +101,7 @@ import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; +import me.leolin.shortcutbadger.ShortcutBadger; public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener { @@ -118,6 +120,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa startService(intent); } }; + + private final SerialSingleThreadExecutor mFileAddingExecutor = new SerialSingleThreadExecutor(); + private final SerialSingleThreadExecutor mDatabaseExecutor = new SerialSingleThreadExecutor(); + private final IBinder mBinder = new XmppConnectionBinder(); private final List conversations = new CopyOnWriteArrayList<>(); private final FileObserver fileObserver = new FileObserver( @@ -203,6 +209,18 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private OnMessagePacketReceived mMessageParser = new MessageParser(this); private OnPresencePacketReceived mPresenceParser = new PresenceParser(this); private IqParser mIqParser = new IqParser(this); + private OnIqPacketReceived mDefaultIqHandler = new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE.ERROR) { + Element error = packet.findChild("error"); + String text = error != null ? error.findChildContent("text") : null; + if (text != null) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received iq error - "+text); + } + } + } + }; private MessageGenerator mMessageGenerator = new MessageGenerator(this); private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); private List accounts; @@ -214,7 +232,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private final List mInProgressAvatarFetches = new ArrayList<>(); private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); private OnConversationUpdate mOnConversationUpdate = null; - private Integer convChangedListenerCount = 0; + private int convChangedListenerCount = 0; + private int unreadCount = 0; private OnAccountUpdate mOnAccountUpdate = null; private OnStatusChanged statusListener = new OnStatusChanged() { @@ -359,7 +378,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa callback.success(message); } } else { - new Thread(new Runnable() { + mFileAddingExecutor.execute(new Runnable() { @Override public void run() { try { @@ -374,8 +393,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa callback.error(e.getResId(),message); } } - }).start(); - + }); } } @@ -391,7 +409,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } message.setCounterpart(conversation.getNextCounterpart()); message.setType(Message.TYPE_IMAGE); - new Thread(new Runnable() { + mFileAddingExecutor.execute(new Runnable() { @Override public void run() { @@ -406,7 +424,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa callback.error(e.getResId(), message); } } - }).start(); + }); } public Conversation find(Bookmark bookmark) { @@ -485,8 +503,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa long pingTimeoutIn = (lastSent + Config.PING_TIMEOUT * 1000) - SystemClock.elapsedRealtime(); if (lastSent > lastReceived) { if (pingTimeoutIn < 0) { - long age = (SystemClock.elapsedRealtime() - account.getXmppConnection().getLastConnect()) / 1000; - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout. connection age was: "+age+"s"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ping timeout"); this.reconnectAccount(account, true); } else { int secs = (int) (pingTimeoutIn / 1000); @@ -573,7 +590,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa this.accounts = databaseBackend.getAccounts(); for (final Account account : this.accounts) { - account.initOtrEngine(this); + account.initAccountServices(this); } restoreFromDatabase(); @@ -902,7 +919,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (Bookmark bookmark : account.getBookmarks()) { storage.addChild(bookmark); } - sendIqPacket(account, iqPacket, null); + sendIqPacket(account, iqPacket, mDefaultIqHandler); } public void onPhoneContactsLoaded(final List phoneContacts) { @@ -914,7 +931,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void run() { Log.d(Config.LOGTAG,"start merging phone contacts with roster"); for (Account account : accounts) { - account.getRoster().clearSystemAccounts(); + List withSystemAccounts = account.getRoster().getWithSystemAccounts(); for (Bundle phoneContact : phoneContacts) { if (Thread.interrupted()) { Log.d(Config.LOGTAG,"interrupted merging phone contacts"); @@ -931,9 +948,18 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa + "#" + phoneContact.getString("lookup"); contact.setSystemAccount(systemAccount); - contact.setPhotoUri(phoneContact.getString("photouri")); - getAvatarService().clear(contact); + if (contact.setPhotoUri(phoneContact.getString("photouri"))) { + getAvatarService().clear(contact); + } contact.setSystemName(phoneContact.getString("displayname")); + withSystemAccounts.remove(contact); + } + for(Contact contact : withSystemAccounts) { + contact.setSystemAccount(null); + contact.setSystemName(null); + if (contact.setPhotoUri(null)) { + getAvatarService().clear(contact); + } } } Log.d(Config.LOGTAG,"finished merging phone contacts"); @@ -954,7 +980,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Account account = accountLookupTable.get(conversation.getAccountUuid()); conversation.setAccount(account); } - new Thread(new Runnable() { + Runnable runnable =new Runnable() { @Override public void run() { Log.d(Config.LOGTAG,"restoring roster"); @@ -975,7 +1001,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Log.d(Config.LOGTAG,"restored all messages"); updateConversationUi(); } - }).start(); + }; + mDatabaseExecutor.execute(runnable); } } @@ -1040,11 +1067,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { - Log.d(Config.LOGTAG,"load more messages for "+conversation.getName() + " prior to "+MessageGenerator.getTimestamp(timestamp)); + Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) { return; } - new Thread(new Runnable() { + Runnable runnable = new Runnable() { @Override public void run() { final Account account = conversation.getAccount(); @@ -1063,7 +1090,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa callback.informUser(R.string.fetching_history_from_server); } } - }).start(); + }; + mDatabaseExecutor.execute(runnable); } public List getAccounts() { @@ -1175,7 +1203,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void createAccount(final Account account) { - account.initOtrEngine(this); + account.initAccountServices(this); databaseBackend.createAccount(account); this.accounts.add(account); this.reconnectAccountInBackground(account); @@ -1690,7 +1718,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } IqPacket request = this.mIqGenerator.changeAffiliation(conference, jids, after.toString()); - sendIqPacket(conference.getAccount(), request, null); + sendIqPacket(conference.getAccount(), request, mDefaultIqHandler); } public void changeRoleInConference(final Conversation conference, final String nick, MucOptions.Role role, final OnRoleChanged callback) { @@ -1834,7 +1862,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); iq.query(Xmlns.ROSTER).addChild(contact.asElement()); - account.getXmppConnection().sendIqPacket(iq, null); + account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); if (sendUpdates) { sendPresencePacket(account, mPresenceGenerator.sendPresenceUpdatesTo(contact)); @@ -1992,17 +2020,16 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private void fetchAvatarVcard(final Account account, final Avatar avatar, final UiCallback callback) { IqPacket packet = this.mIqGenerator.retrieveVcardAvatar(avatar); - this.sendIqPacket(account,packet,new OnIqPacketReceived() { + this.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { - synchronized(mInProgressAvatarFetches) { - mInProgressAvatarFetches.remove(generateFetchKey(account,avatar)); + synchronized (mInProgressAvatarFetches) { + mInProgressAvatarFetches.remove(generateFetchKey(account, avatar)); } if (packet.getType() == IqPacket.TYPE.RESULT) { - Element vCard = packet.findChild("vCard","vcard-temp"); + Element vCard = packet.findChild("vCard", "vcard-temp"); Element photo = vCard != null ? vCard.findChild("PHOTO") : null; - Element binval = photo != null ? photo.findChild("BINVAL") : null; - String image = binval != null ? binval.getContent() : null; + String image = photo != null ? photo.findChildContent("BINVAL") : null; if (image != null) { avatar.image = image; if (getFileBackend().save(avatar)) { @@ -2065,7 +2092,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Element item = iq.query(Xmlns.ROSTER).addChild("item"); item.setAttribute("jid", contact.getJid().toString()); item.setAttribute("subscription", "remove"); - account.getXmppConnection().sendIqPacket(iq, null); + account.getXmppConnection().sendIqPacket(iq, mDefaultIqHandler); } } @@ -2118,7 +2145,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void directInvite(Conversation conversation, Jid jid) { - MessagePacket packet = mMessageGenerator.directInvite(conversation,jid); + MessagePacket packet = mMessageGenerator.directInvite(conversation, jid); sendMessagePacket(conversation.getAccount(),packet); } @@ -2262,6 +2289,20 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void markRead(final Conversation conversation) { mNotificationService.clear(conversation); conversation.markRead(); + updateUnreadCountBadge(); + } + + public synchronized void updateUnreadCountBadge() { + int count = unreadCount(); + if (unreadCount != count) { + Log.d(Config.LOGTAG, "update unread count to " + count); + if (count > 0) { + ShortcutBadger.with(getApplicationContext()).count(count); + } else { + ShortcutBadger.with(getApplicationContext()).remove(); + } + unreadCount = count; + } } public void sendReadMarker(final Conversation conversation) { @@ -2309,13 +2350,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public void syncRosterToDisk(final Account account) { - new Thread(new Runnable() { + Runnable runnable = new Runnable() { @Override public void run() { databaseBackend.writeRoster(account.getRoster()); } - }).start(); + }; + mDatabaseExecutor.execute(runnable); } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index f7156d7a..c190caed 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -10,6 +10,7 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; +import android.provider.ContactsContract; import android.provider.ContactsContract.CommonDataKinds; import android.provider.ContactsContract.Contacts; import android.provider.ContactsContract.Intents; @@ -126,14 +127,23 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd @Override public void onClick(View v) { - AlertDialog.Builder builder = new AlertDialog.Builder( - ContactDetailsActivity.this); - builder.setTitle(getString(R.string.action_add_phone_book)); - builder.setMessage(getString(R.string.add_phone_book_text, + if (contact.getSystemAccount() == null) { + AlertDialog.Builder builder = new AlertDialog.Builder( + ContactDetailsActivity.this); + builder.setTitle(getString(R.string.action_add_phone_book)); + builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid())); - builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.add), addToPhonebook); - builder.create().show(); + builder.setNegativeButton(getString(R.string.cancel), null); + builder.setPositiveButton(getString(R.string.add), addToPhonebook); + builder.create().show(); + } else { + String[] systemAccount = contact.getSystemAccount().split("#"); + long id = Long.parseLong(systemAccount[0]); + Uri uri = ContactsContract.Contacts.getLookupUri(id, systemAccount[1]); + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(uri); + startActivity(intent); + } } }; @@ -340,12 +350,9 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } else { contactJidTv.setText(contact.getJid().toString()); } - accountJidTv.setText(getString(R.string.using_account, contact - .getAccount().getJid().toBareJid())); - prepareContactBadge(badge, contact); - if (contact.getSystemAccount() == null) { - badge.setOnClickListener(onBadgeClick); - } + accountJidTv.setText(getString(R.string.using_account, contact.getAccount().getJid().toBareJid())); + badge.setImageBitmap(avatarService().get(contact, getPixel(72))); + badge.setOnClickListener(this.onBadgeClick); keys.removeAllViews(); boolean hasKeys = false; @@ -419,15 +426,6 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } } - private void prepareContactBadge(QuickContactBadge badge, Contact contact) { - if (contact.getSystemAccount() != null) { - String[] systemAccount = contact.getSystemAccount().split("#"); - long id = Long.parseLong(systemAccount[0]); - badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1])); - } - badge.setImageBitmap(avatarService().get(contact, getPixel(72))); - } - protected void confirmToDeleteFingerprint(final String fingerprint) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.delete_fingerprint); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index d9f56c5d..c48b5865 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -479,6 +479,9 @@ public class ConversationActivity extends XmppActivity case ATTACHMENT_CHOICE_TAKE_PHOTO: getPreferences().edit().putString("recently_used_quick_action","photo").apply(); break; + case ATTACHMENT_CHOICE_CHOOSE_IMAGE: + getPreferences().edit().putString("recently_used_quick_action","picture").apply(); + break; } final Conversation conversation = getSelectedConversation(); final int encryption = conversation.getNextEncryption(forceEncryption()); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 02f664db..a817b27b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -256,6 +256,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case RECORD_VOICE: activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_RECORD_VOICE); break; + case CHOOSE_PICTURE: + activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE); + break; case CANCEL: if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { conversation.setNextCounterpart(null); @@ -818,7 +821,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateChatMsgHint(); } - enum SendButtonAction {TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL} + enum SendButtonAction {TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE} private int getSendButtonImageResource(SendButtonAction action, int status) { switch (action) { @@ -887,6 +890,19 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa default: return R.drawable.ic_send_cancel_offline; } + case CHOOSE_PICTURE: + switch (status) { + case Presences.CHAT: + case Presences.ONLINE: + return R.drawable.ic_send_picture_online; + case Presences.AWAY: + return R.drawable.ic_send_picture_away; + case Presences.XA: + case Presences.DND: + return R.drawable.ic_send_picture_dnd; + default: + return R.drawable.ic_send_picture_offline; + } } return R.drawable.ic_send_text_offline; } @@ -920,6 +936,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case "voice": action = SendButtonAction.RECORD_VOICE; break; + case "picture": + action = SendButtonAction.CHOOSE_PICTURE; + break; default: action = SendButtonAction.TEXT; break; diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index 42dd1c95..bb354ae0 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -20,11 +20,19 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Random; import java.util.TreeMap; +import java.util.regex.Pattern; import android.os.Bundle; import android.util.Log; public class DNSHelper { + + public static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + public static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + public static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + public static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + public static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + protected static Client client = new Client(); public static Bundle getSRVRecord(final Jid jid) throws IOException { @@ -37,9 +45,6 @@ public class DNSHelper { Bundle b = queryDNS(host, ip); if (b.containsKey("values")) { return b; - } else if (b.containsKey("error") - && "nosrv".equals(b.getString("error", null))) { - return b; } } } @@ -50,113 +55,95 @@ public class DNSHelper { Bundle bundle = new Bundle(); try { String qname = "_xmpp-client._tcp." + host; - Log.d(Config.LOGTAG, - "using dns server: " + dnsServer.getHostAddress() - + " to look up " + host); - DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, - dnsServer.getHostAddress()); - - // How should we handle priorities and weight? - // Wikipedia has a nice article about priorities vs. weights: - // https://en.wikipedia.org/wiki/SRV_record#Provisioning_for_high_service_availability - - // we bucket the SRV records based on priority, pick per priority - // a random order respecting the weight, and dump that priority by - // priority + Log.d(Config.LOGTAG, "using dns server: " + dnsServer.getHostAddress() + " to look up " + host); + DNSMessage message = client.query(qname, TYPE.SRV, CLASS.IN, dnsServer.getHostAddress()); TreeMap> priorities = new TreeMap<>(); TreeMap> ips4 = new TreeMap<>(); TreeMap> ips6 = new TreeMap<>(); - for (Record[] rrset : new Record[][] { message.getAnswers(), - message.getAdditionalResourceRecords() }) { + for (Record[] rrset : new Record[][] { message.getAnswers(), message.getAdditionalResourceRecords() }) { for (Record rr : rrset) { Data d = rr.getPayload(); - if (d instanceof SRV - && NameUtil.idnEquals(qname, rr.getName())) { + if (d instanceof SRV && NameUtil.idnEquals(qname, rr.getName())) { SRV srv = (SRV) d; if (!priorities.containsKey(srv.getPriority())) { - priorities.put(srv.getPriority(), - new ArrayList(2)); + priorities.put(srv.getPriority(),new ArrayList()); } priorities.get(srv.getPriority()).add(srv); } if (d instanceof A) { - A arecord = (A) d; + A a = (A) d; if (!ips4.containsKey(rr.getName())) { - ips4.put(rr.getName(), new ArrayList(3)); + ips4.put(rr.getName(), new ArrayList()); } - ips4.get(rr.getName()).add(arecord.toString()); + ips4.get(rr.getName()).add(a.toString()); } if (d instanceof AAAA) { AAAA aaaa = (AAAA) d; if (!ips6.containsKey(rr.getName())) { - ips6.put(rr.getName(), new ArrayList(3)); + ips6.put(rr.getName(), new ArrayList()); } ips6.get(rr.getName()).add("[" + aaaa.toString() + "]"); } } } - Random rnd = new Random(); - ArrayList result = new ArrayList<>( - priorities.size() * 2 + 1); + ArrayList result = new ArrayList<>(); for (ArrayList s : priorities.values()) { - - // trivial case - if (s.size() <= 1) { - result.addAll(s); - continue; - } - - long totalweight = 0l; - for (SRV srv : s) { - totalweight += srv.getWeight(); - } - - while (totalweight > 0l && s.size() > 0) { - long p = (rnd.nextLong() & 0x7fffffffffffffffl) - % totalweight; - int i = 0; - while (p > 0) { - p -= s.get(i++).getPriority(); - } - if (i>0) i--; - // remove is expensive, but we have only a few entries - // anyway - SRV srv = s.remove(i); - totalweight -= srv.getWeight(); - result.add(srv); - } - - Collections.shuffle(s, rnd); result.addAll(s); - } + ArrayList values = new ArrayList<>(); if (result.size() == 0) { - bundle.putString("error", "nosrv"); + DNSMessage response; + response = client.query(host, TYPE.A, CLASS.IN, dnsServer.getHostAddress()); + for(int i = 0; i < response.getAnswers().length; ++i) { + values.add(createNamePortBundle(host,5222,response.getAnswers()[i].getPayload())); + } + response = client.query(host, TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); + for(int i = 0; i < response.getAnswers().length; ++i) { + values.add(createNamePortBundle(host,5222,response.getAnswers()[i].getPayload())); + } + bundle.putParcelableArrayList("values", values); return bundle; } - ArrayList values = new ArrayList<>(); for (SRV srv : result) { if (ips6.containsKey(srv.getName())) { values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6)); + } else { + DNSMessage response = client.query(srv.getName(), TYPE.AAAA, CLASS.IN, dnsServer.getHostAddress()); + for(int i = 0; i < response.getAnswers().length; ++i) { + values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload())); + } } if (ips4.containsKey(srv.getName())) { values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4)); + } else { + DNSMessage response = client.query(srv.getName(), TYPE.A, CLASS.IN, dnsServer.getHostAddress()); + for(int i = 0; i < response.getAnswers().length; ++i) { + values.add(createNamePortBundle(srv.getName(),srv.getPort(),response.getAnswers()[i].getPayload())); + } } - values.add(createNamePortBundle(srv.getName(),srv.getPort(),null)); + values.add(createNamePortBundle(srv.getName(), srv.getPort())); } bundle.putParcelableArrayList("values", values); } catch (SocketTimeoutException e) { bundle.putString("error", "timeout"); } catch (Exception e) { + Log.d(Config.LOGTAG,e.getMessage()); bundle.putString("error", "unhandled"); } return bundle; } + private static Bundle createNamePortBundle(String name, int port) { + Bundle namePort = new Bundle(); + namePort.putString("name", name); + namePort.putInt("port", port); + return namePort; + } + private static Bundle createNamePortBundle(String name, int port, TreeMap> ips) { Bundle namePort = new Bundle(); namePort.putString("name", name); @@ -169,15 +156,23 @@ public class DNSHelper { return namePort; } - final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); - - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + private static Bundle createNamePortBundle(String name, int port, Data data) { + Bundle namePort = new Bundle(); + namePort.putString("name", name); + namePort.putInt("port", port); + if (data instanceof A) { + namePort.putString("ip", data.toString()); + } else if (data instanceof AAAA) { + namePort.putString("ip","["+data.toString()+"]"); } - return new String(hexChars); + return namePort; + } + + public static boolean isIp(final String server) { + return PATTERN_IPV4.matcher(server).matches() + || PATTERN_IPV6.matcher(server).matches() + || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() + || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() + || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches(); } } diff --git a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java index b31b9018..74f91a98 100644 --- a/src/main/java/eu/siacs/conversations/utils/GeoHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/GeoHelper.java @@ -54,8 +54,14 @@ public class GeoHelper { 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()); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + if (message.getStatus() == Message.STATUS_RECEIVED) { + locationPluginIntent.putExtra("name",conversation.getName()); + locationPluginIntent.putExtra("jid",message.getCounterpart().toString()); + } + else { + locationPluginIntent.putExtra("jid",conversation.getAccount().getJid().toString()); + } } intents.add(locationPluginIntent); diff --git a/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java new file mode 100644 index 00000000..bfb4668d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/SerialSingleThreadExecutor.java @@ -0,0 +1,34 @@ +package eu.siacs.conversations.utils; + +import java.util.ArrayDeque; +import java.util.Queue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class SerialSingleThreadExecutor implements Executor { + + final Executor executor = Executors.newSingleThreadExecutor(); + final Queue tasks = new ArrayDeque(); + Runnable active; + + public synchronized void execute(final Runnable r) { + tasks.offer(new Runnable() { + public void run() { + try { + r.run(); + } finally { + scheduleNext(); + } + } + }); + if (active == null) { + scheduleNext(); + } + } + + protected synchronized void scheduleNext() { + if ((active = tasks.poll()) != null) { + executor.execute(active); + } + } +} \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 51708759..32657c66 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -57,6 +57,11 @@ public class Element { return null; } + public String findChildContent(String name) { + Element element = findChild(name); + return element == null ? null : element.getContent(); + } + public Element findChild(String name, String xmlns) { for (Element child : this.children) { if (child.getName().equals(name) @@ -67,6 +72,11 @@ public class Element { return null; } + public String findChildContent(String name, String xmlns) { + Element element = findChild(name,xmlns); + return element == null ? null : element.getContent(); + } + public boolean hasChild(final String name) { return findChild(name) != null; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index d52ebee5..8a438906 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -26,6 +26,7 @@ import java.net.IDN; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; +import java.net.SocketAddress; import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; @@ -155,56 +156,62 @@ public class XmppConnection implements Runnable { tagWriter = new TagWriter(); packetCallbacks.clear(); this.changeStatus(Account.State.CONNECTING); - final Bundle result = DNSHelper.getSRVRecord(account.getServer()); - final ArrayList values = result.getParcelableArrayList("values"); - if ("timeout".equals(result.getString("error"))) { - throw new IOException("timeout in dns"); - } else if (values != null) { - int i = 0; - boolean socketError = true; - while (socketError && values.size() > i) { - final Bundle namePort = (Bundle) values.get(i); - try { - String srvRecordServer; - try { - srvRecordServer = IDN.toASCII(namePort.getString("name")); - } catch (final IllegalArgumentException e) { - // TODO: Handle me?` - srvRecordServer = ""; - } - final int srvRecordPort = namePort.getInt("port"); - final String srvIpServer = namePort.getString("ip"); - final InetSocketAddress addr; - if (srvIpServer != null) { - addr = new InetSocketAddress(srvIpServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": using values from dns " + srvRecordServer - + "[" + srvIpServer + "]:" + srvRecordPort); - } else { - addr = new InetSocketAddress(srvRecordServer, srvRecordPort); - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() - + ": using values from dns " - + srvRecordServer + ":" + srvRecordPort); - } - socket = new Socket(); - socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); - socketError = false; - } catch (final UnknownHostException e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); - i++; - } catch (final IOException e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); - i++; - } - } - if (socketError) { + if (DNSHelper.isIp(account.getServer().toString())) { + socket = new Socket(); + try { + socket.connect(new InetSocketAddress(account.getServer().toString(), 5222), Config.SOCKET_TIMEOUT * 1000); + } catch (IOException e) { throw new UnknownHostException(); } - } else if (result.containsKey("error") - && "nosrv".equals(result.getString("error", null))) { - socket = new Socket(account.getServer().getDomainpart(), 5222); } else { - throw new IOException("unhandled exception in DNS resolver"); + final Bundle result = DNSHelper.getSRVRecord(account.getServer()); + final ArrayList values = result.getParcelableArrayList("values"); + if ("timeout".equals(result.getString("error"))) { + throw new IOException("timeout in dns"); + } else if (values != null) { + int i = 0; + boolean socketError = true; + while (socketError && values.size() > i) { + final Bundle namePort = (Bundle) values.get(i); + try { + String srvRecordServer; + try { + srvRecordServer = IDN.toASCII(namePort.getString("name")); + } catch (final IllegalArgumentException e) { + // TODO: Handle me?` + srvRecordServer = ""; + } + final int srvRecordPort = namePort.getInt("port"); + final String srvIpServer = namePort.getString("ip"); + final InetSocketAddress addr; + if (srvIpServer != null) { + addr = new InetSocketAddress(srvIpServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": using values from dns " + srvRecordServer + + "[" + srvIpServer + "]:" + srvRecordPort); + } else { + addr = new InetSocketAddress(srvRecordServer, srvRecordPort); + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + + ": using values from dns " + + srvRecordServer + ":" + srvRecordPort); + } + socket = new Socket(); + socket.connect(addr, Config.SOCKET_TIMEOUT * 1000); + socketError = false; + } catch (final UnknownHostException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + i++; + } catch (final IOException e) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); + i++; + } + } + if (socketError) { + throw new UnknownHostException(); + } + } else { + throw new IOException("unhandled exception in DNS resolver"); + } } final OutputStream out = socket.getOutputStream(); tagWriter.setOutputStream(out); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java index 29efcf8f..8da53c1b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -160,13 +160,18 @@ public class JingleInbandTransport extends JingleTransport { byte[] buffer = new byte[this.bufferSize]; try { int count = fileInputStream.read(buffer); - this.remainingSize -= count; - if (count != buffer.length && count != -1) { + if (count == -1) { + file.setSha1Sum(CryptoHelper.bytesToHex(digest.digest())); + this.onFileTransmissionStatusChanged.onFileTransmitted(file); + fileInputStream.close(); + return; + } else if (count != buffer.length) { int rem = fileInputStream.read(buffer,count,buffer.length-count); if (rem > 0) { count += rem; } } + this.remainingSize -= count; this.digest.update(buffer,0,count); String base64 = Base64.encodeToString(buffer,0,count, Base64.NO_WRAP); IqPacket iq = new IqPacket(IqPacket.TYPE.SET); diff --git a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java index 04d55bbe..74da6a9b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java +++ b/src/main/java/eu/siacs/conversations/xmpp/pep/Avatar.java @@ -83,8 +83,7 @@ public class Avatar { } public static Avatar parsePresence(Element x) { - Element photo = x != null ? x.findChild("photo") : null; - String hash = photo != null ? photo.getContent() : null; + String hash = x == null ? null : x.findChildContent("photo"); if (hash == null) { return null; } diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java index 55256ece..bd706b57 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java @@ -51,4 +51,8 @@ public class AbstractStanza extends Element { || getTo().equals(account.getJid().toBareJid()) || getTo().equals(account.getJid()); } + + public boolean fromAccount(final Account account) { + return getFrom() != null && getFrom().toBareJid().equals(account.getJid().toBareJid()); + } } 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 93aaa68c..e32811af 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/MessagePacket.java @@ -1,5 +1,10 @@ package eu.siacs.conversations.xmpp.stanzas; +import android.util.Pair; + +import java.text.ParseException; + +import eu.siacs.conversations.parser.AbstractParser; import eu.siacs.conversations.xml.Element; public class MessagePacket extends AbstractStanza { @@ -14,12 +19,7 @@ public class MessagePacket extends AbstractStanza { } public String getBody() { - Element body = this.findChild("body"); - if (body != null) { - return body.getContent(); - } else { - return null; - } + return findChildContent("body"); } public void setBody(String text) { @@ -66,4 +66,31 @@ public class MessagePacket extends AbstractStanza { return TYPE_NORMAL; } } + + public Pair getForwardedMessagePacket(String name, String namespace) { + Element wrapper = findChild(name, namespace); + if (wrapper == null) { + return null; + } + Element forwarded = wrapper.findChild("forwarded", "urn:xmpp:forward:0"); + if (forwarded == null) { + return null; + } + MessagePacket packet = create(forwarded.findChild("message")); + if (packet == null) { + return null; + } + Long timestamp = AbstractParser.getTimestamp(forwarded,null); + return new Pair(packet,timestamp); + } + + public static MessagePacket create(Element element) { + if (element == null) { + return null; + } + MessagePacket packet = new MessagePacket(); + packet.setAttributes(element.getAttributes()); + packet.setChildren(element.getChildren()); + return packet; + } } diff --git a/src/main/res/drawable-hdpi/ic_send_picture_away.png b/src/main/res/drawable-hdpi/ic_send_picture_away.png new file mode 100644 index 00000000..09d1c7b7 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_picture_away.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_picture_dnd.png b/src/main/res/drawable-hdpi/ic_send_picture_dnd.png new file mode 100644 index 00000000..77964b5b Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_picture_dnd.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_picture_offline.png b/src/main/res/drawable-hdpi/ic_send_picture_offline.png new file mode 100644 index 00000000..28135e25 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_picture_offline.png differ diff --git a/src/main/res/drawable-hdpi/ic_send_picture_online.png b/src/main/res/drawable-hdpi/ic_send_picture_online.png new file mode 100644 index 00000000..feb926c6 Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_send_picture_online.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_picture_away.png b/src/main/res/drawable-mdpi/ic_send_picture_away.png new file mode 100644 index 00000000..d3ebca53 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_picture_away.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_picture_dnd.png b/src/main/res/drawable-mdpi/ic_send_picture_dnd.png new file mode 100644 index 00000000..1d293f20 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_picture_dnd.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_picture_offline.png b/src/main/res/drawable-mdpi/ic_send_picture_offline.png new file mode 100644 index 00000000..95d5621e Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_picture_offline.png differ diff --git a/src/main/res/drawable-mdpi/ic_send_picture_online.png b/src/main/res/drawable-mdpi/ic_send_picture_online.png new file mode 100644 index 00000000..be4194d3 Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_send_picture_online.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_picture_away.png b/src/main/res/drawable-xhdpi/ic_send_picture_away.png new file mode 100644 index 00000000..f9aa21dc Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_picture_away.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_picture_dnd.png b/src/main/res/drawable-xhdpi/ic_send_picture_dnd.png new file mode 100644 index 00000000..95e4acce Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_picture_dnd.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_picture_offline.png b/src/main/res/drawable-xhdpi/ic_send_picture_offline.png new file mode 100644 index 00000000..75ff2fc4 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_picture_offline.png differ diff --git a/src/main/res/drawable-xhdpi/ic_send_picture_online.png b/src/main/res/drawable-xhdpi/ic_send_picture_online.png new file mode 100644 index 00000000..0f68d5f5 Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_send_picture_online.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_picture_away.png b/src/main/res/drawable-xxhdpi/ic_send_picture_away.png new file mode 100644 index 00000000..7898ed4f Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_picture_away.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_picture_dnd.png b/src/main/res/drawable-xxhdpi/ic_send_picture_dnd.png new file mode 100644 index 00000000..ccffabbe Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_picture_dnd.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_picture_offline.png b/src/main/res/drawable-xxhdpi/ic_send_picture_offline.png new file mode 100644 index 00000000..7b5687e4 Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_picture_offline.png differ diff --git a/src/main/res/drawable-xxhdpi/ic_send_picture_online.png b/src/main/res/drawable-xxhdpi/ic_send_picture_online.png new file mode 100644 index 00000000..82eab70c Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_send_picture_online.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_picture_away.png b/src/main/res/drawable-xxxhdpi/ic_send_picture_away.png new file mode 100644 index 00000000..1daa8ecc Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_picture_away.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_picture_dnd.png b/src/main/res/drawable-xxxhdpi/ic_send_picture_dnd.png new file mode 100644 index 00000000..d8257aad Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_picture_dnd.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_picture_offline.png b/src/main/res/drawable-xxxhdpi/ic_send_picture_offline.png new file mode 100644 index 00000000..d487709b Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_picture_offline.png differ diff --git a/src/main/res/drawable-xxxhdpi/ic_send_picture_online.png b/src/main/res/drawable-xxxhdpi/ic_send_picture_online.png new file mode 100644 index 00000000..c095d795 Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_send_picture_online.png differ diff --git a/src/main/res/values-bg/strings.xml b/src/main/res/values-bg/strings.xml index b27481dd..52bb0054 100644 --- a/src/main/res/values-bg/strings.xml +++ b/src/main/res/values-bg/strings.xml @@ -446,4 +446,9 @@ Изберете %d контакт Изберете %d контакта + Замяна на бутона за изпращане с бързо действие + Бързо действие + Нищо + Използвани наскоро + Изберете бързо действие diff --git a/src/main/res/values-ca@valencia/strings.xml b/src/main/res/values-ca@valencia/strings.xml deleted file mode 100644 index c757504a..00000000 --- a/src/main/res/values-ca@valencia/strings.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index f49cb1b9..c199ec76 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -448,4 +448,9 @@ Vybrat %d kontakty Vybrat %d kontaktů + Nahradit tlačítko odeslání rychlou akcí + Rychlá akce + Žádná + Naposledy použitá + Vybrat rychlou akci diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 08b0d4a5..000c253b 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -446,4 +446,9 @@ %d Kontakt ausgewählt %d Kontakte ausgewählt + Ersetze Absende-Knopf durch Schnell-Tasten + Schnell-Tasten + keine + zuletzt verwendet + wähle Schnell-Taste diff --git a/src/main/res/values-es/strings.xml b/src/main/res/values-es/strings.xml index 109715e3..880ee66b 100644 --- a/src/main/res/values-es/strings.xml +++ b/src/main/res/values-es/strings.xml @@ -446,4 +446,9 @@ Seleccionado %d contacto Seleccionados %d contactos + Cambiar el botón de enviar por botón de acción rápida + Acción Rápida + Ninguna + Usada más recientemente + Elegir acción rápida diff --git a/src/main/res/values-eu/strings.xml b/src/main/res/values-eu/strings.xml index ac66bfc1..b594c069 100644 --- a/src/main/res/values-eu/strings.xml +++ b/src/main/res/values-eu/strings.xml @@ -446,4 +446,9 @@ Hautatu kontaktu %d Hautatu %d kontaktu + Bidaltze botoia ekintza azkar batekin aldatu + Ekintza azkarra + Bat ere ez + Azkenengo aldiz erabilitakoa + Ekintza azkarra aukeratu diff --git a/src/main/res/values-id/strings.xml b/src/main/res/values-id/strings.xml index 69bd0911..3ce9cc82 100644 --- a/src/main/res/values-id/strings.xml +++ b/src/main/res/values-id/strings.xml @@ -444,4 +444,9 @@ Pilih %d kontak + Timpa tombol kirim dengan aksi cepat + Aksi Cepat + Tak satupun + Maling sering digunakan + Pilih aksi cepat diff --git a/src/main/res/values-ja/strings.xml b/src/main/res/values-ja/strings.xml index 35fb5472..38735f39 100644 --- a/src/main/res/values-ja/strings.xml +++ b/src/main/res/values-ja/strings.xml @@ -444,4 +444,9 @@ %d 連絡先を選択 + 送信ボタンをクイックアクションで置き換えます + クイックアクション + なし + 最近使用した + クイックアクションの選択 diff --git a/src/main/res/values-ko/strings.xml b/src/main/res/values-ko/strings.xml index d00ab0fd..9354c432 100644 --- a/src/main/res/values-ko/strings.xml +++ b/src/main/res/values-ko/strings.xml @@ -444,4 +444,9 @@ %d 연락처 선택 + 전송 버튼을 빠른 동작 버튼으로 교체 + 빠른 동작 + 없음 + 최근 사용된 항목 + 빠른 동작 선택 diff --git a/src/main/res/values-nl/strings.xml b/src/main/res/values-nl/strings.xml index 2532130d..e3f0e69b 100644 --- a/src/main/res/values-nl/strings.xml +++ b/src/main/res/values-nl/strings.xml @@ -446,4 +446,9 @@ Selecteer %d contact Selecteer %d contacten + Vervang verzendknop door snelle actie + Snelle actie + Geen + Recent gebruikt + Kies snelle actie diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 70c3a49d..6a983f50 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -448,4 +448,9 @@ %d kontakty wybrane %d kontaktów wybranych + Zastąp przycisk wysyłania szybką akcją + Szybka akcja + Brak + Ostatnio używana + Wybierz szybką akcję diff --git a/src/main/res/values-sr/strings.xml b/src/main/res/values-sr/strings.xml index ea5cbdb8..60f2e586 100644 --- a/src/main/res/values-sr/strings.xml +++ b/src/main/res/values-sr/strings.xml @@ -448,4 +448,9 @@ Изабери %d контакта Изабери %d контаката + Замени дугме за слање брзом радњом + Брза радња + Ниједна + Недавно коришћена + Изаберите брзу радњу diff --git a/src/main/res/values-sv/strings.xml b/src/main/res/values-sv/strings.xml index 4f84110b..9c60db53 100644 --- a/src/main/res/values-sv/strings.xml +++ b/src/main/res/values-sv/strings.xml @@ -446,4 +446,9 @@ Välj %d kontakt Välj %d kontakter + Byt sänd-knappen mot snabbfunktion + Snabbfunktion + Ingen + Senast använd + Välj snabbfunktion diff --git a/src/main/res/values-zh-rCN/strings.xml b/src/main/res/values-zh-rCN/strings.xml index 2f80e064..e194bffd 100644 --- a/src/main/res/values-zh-rCN/strings.xml +++ b/src/main/res/values-zh-rCN/strings.xml @@ -444,4 +444,9 @@ 选择 %d 个联系人 + 以快速动作替代发送按钮 + 快速动作 + + 最近使用过的 + 选择快速动作 diff --git a/src/main/res/values/arrays.xml b/src/main/res/values/arrays.xml index fec077cc..5be352d1 100644 --- a/src/main/res/values/arrays.xml +++ b/src/main/res/values/arrays.xml @@ -44,6 +44,7 @@ @string/none @string/recently_used @string/attach_take_picture + @string/attach_choose_picture @string/attach_record_voice @string/send_location @@ -52,6 +53,7 @@ none recent photo + picture voice location