diff options
Diffstat (limited to '')
22 files changed, 509 insertions, 144 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 75715045..a1da283f 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -31,6 +31,8 @@ public final class Config { public static final int CARBON_GRACE_PERIOD = 90; public static final int MINI_GRACE_PERIOD = 750; + public static final boolean CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND = false; + public static final int AVATAR_SIZE = 192; public static final Bitmap.CompressFormat AVATAR_FORMAT = Bitmap.CompressFormat.PNG; @@ -112,6 +114,5 @@ public final class Config { }; private Config() { - } } diff --git a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java index 40baa479..d9464f8c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/PgpEngine.java @@ -40,25 +40,24 @@ public class PgpEngine { this.mXmppConnectionService = service; } - public void decrypt(final Message message, - final UiCallback<Message> callback) { + public void decrypt(final Message message, final UiCallback<Message> callback) { Intent params = new Intent(); params.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); + final String uuid = message.getUuid(); if (message.getType() == Message.TYPE_TEXT) { - InputStream is = new ByteArrayInputStream(message.getBody() - .getBytes()); + InputStream is = new ByteArrayInputStream(message.getBody().getBytes()); final OutputStream os = new ByteArrayOutputStream(); api.executeApiAsync(params, is, os, new IOpenPgpCallback() { @Override public void onReturn(Intent result) { notifyPgpDecryptionService(message.getConversation().getAccount(), OpenPgpApi.ACTION_DECRYPT_VERIFY, result); - switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, - OpenPgpApi.RESULT_CODE_ERROR)) { + switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { case OpenPgpApi.RESULT_CODE_SUCCESS: try { os.flush(); - if (message.getEncryption() == Message.ENCRYPTION_PGP) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { message.setBody(os.toString()); message.setEncryption(Message.ENCRYPTION_DECRYPTED); final HttpConnectionManager manager = mXmppConnectionService.getHttpConnectionManager(); diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index ba1244d1..4eea56b8 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -84,6 +84,7 @@ public class Conversation extends AbstractEntity implements Blockable { private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE; private String mLastReceivedOtrMessageId = null; private String mFirstMamReference = null; + private Message correctingMessage; public boolean hasMessagesLeftOnServer() { return messagesLeftOnServer; @@ -228,6 +229,24 @@ public class Conversation extends AbstractEntity implements Blockable { return null; } + public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { + synchronized (this.messages) { + for(int i = this.messages.size() - 1; i >= 0; --i) { + Message message = messages.get(i); + if (counterpart.equals(message.getCounterpart()) + && ((message.getStatus() == Message.STATUS_RECEIVED) == received) + && (carbon == message.isCarbon() || received) ) { + if (id.equals(message.getRemoteMsgId())) { + return message; + } else { + return null; + } + } + } + } + return null; + } + public Message findSentMessageWithUuid(String id) { synchronized (this.messages) { for (Message message : this.messages) { @@ -296,6 +315,14 @@ public class Conversation extends AbstractEntity implements Blockable { return getLongAttribute("last_clear_history", 0); } + public void setCorrectingMessage(Message correctingMessage) { + this.correctingMessage = correctingMessage; + } + + public Message getCorrectingMessage() { + return this.correctingMessage; + } + public interface OnMessageFound { void onMessageFound(final Message message); } @@ -499,15 +526,18 @@ public class Conversation extends AbstractEntity implements Blockable { return mSmp; } - public void startOtrIfNeeded() { - if (this.otrSession != null - && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { + public boolean startOtrIfNeeded() { + if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { try { this.otrSession.startSession(); + return true; } catch (OtrException e) { this.resetOtrSession(); + return false; } - } + } else { + return true; + } } public boolean endOtrIfNeeded() { diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index f800def1..3f923c41 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -51,6 +51,7 @@ public class Message extends AbstractEntity { public static final String STATUS = "status"; public static final String TYPE = "type"; public static final String CARBON = "carbon"; + public static final String EDITED = "edited"; public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String SERVER_MSG_ID = "serverMsgId"; public static final String RELATIVE_FILE_PATH = "relativeFilePath"; @@ -70,6 +71,7 @@ public class Message extends AbstractEntity { protected int status; protected int type; protected boolean carbon = false; + protected String edited = null; protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; @@ -103,7 +105,8 @@ public class Message extends AbstractEntity { null, null, null, - true); + true, + null); this.conversation = conversation; } @@ -111,7 +114,8 @@ public class Message extends AbstractEntity { final Jid trueCounterpart, final String body, final long timeSent, final int encryption, final int status, final int type, final boolean carbon, final String remoteMsgId, final String relativeFilePath, - final String serverMsgId, final String fingerprint, final boolean read) { + final String serverMsgId, final String fingerprint, final boolean read, + final String edited) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -127,6 +131,7 @@ public class Message extends AbstractEntity { this.serverMsgId = serverMsgId; this.axolotlFingerprint = fingerprint; this.read = read; + this.edited = edited; } public static Message fromCursor(Cursor cursor) { @@ -161,12 +166,13 @@ public class Message extends AbstractEntity { cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(TYPE)), - cursor.getInt(cursor.getColumnIndex(CARBON))>0, + cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), cursor.getString(cursor.getColumnIndex(FINGERPRINT)), - cursor.getInt(cursor.getColumnIndex(READ)) > 0); + cursor.getInt(cursor.getColumnIndex(READ)) > 0, + cursor.getString(cursor.getColumnIndex(EDITED))); } public static Message createStatusMessage(Conversation conversation, String body) { @@ -210,7 +216,8 @@ public class Message extends AbstractEntity { values.put(RELATIVE_FILE_PATH, relativeFilePath); values.put(SERVER_MSG_ID, serverMsgId); values.put(FINGERPRINT, axolotlFingerprint); - values.put(READ,read); + values.put(READ,read ? 1 : 0); + values.put(EDITED, edited); return values; } @@ -339,10 +346,22 @@ public class Message extends AbstractEntity { this.carbon = carbon; } + public void setEdited(String edited) { + this.edited = edited; + } + + public boolean edited() { + return this.edited != null; + } + public void setTrueCounterpart(Jid trueCounterpart) { this.trueCounterpart = trueCounterpart; } + public Jid getTrueCounterpart() { + return this.trueCounterpart; + } + public Transferable getTransferable() { return this.transferable; } @@ -408,6 +427,21 @@ public class Message extends AbstractEntity { } } + public boolean isLastCorrectableMessage() { + Message next = next(); + while(next != null) { + if (next.isCorrectable()) { + return false; + } + next = next.next(); + } + return isCorrectable(); + } + + private boolean isCorrectable() { + return getStatus() != STATUS_RECEIVED && !isCarbon(); + } + public boolean mergeable(final Message message) { return message != null && (message.getType() == Message.TYPE_TEXT && @@ -420,6 +454,7 @@ public class Message extends AbstractEntity { this.getEncryption() == message.getEncryption() && this.getCounterpart() != null && this.getCounterpart().equals(message.getCounterpart()) && + this.edited() == message.edited() && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && !GeoHelper.isGeoUri(message.getBody()) && !GeoHelper.isGeoUri(this.getBody()) && @@ -507,6 +542,14 @@ public class Message extends AbstractEntity { } } + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getEditedId() { + return edited; + } + public enum Decision { MUST, SHOULD, diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 7f4ded11..be9d790d 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -282,7 +282,9 @@ public class MucOptions { } public boolean participating() { - return !online() || self.getRole().ranks(Role.PARTICIPANT); + return !online() + || self.getRole().ranks(Role.PARTICIPANT) + || hasFeature("muc_unmoderated"); } public boolean membersOnly() { diff --git a/src/main/java/eu/siacs/conversations/entities/Presence.java b/src/main/java/eu/siacs/conversations/entities/Presence.java index 69cde832..442f1bca 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presence.java +++ b/src/main/java/eu/siacs/conversations/entities/Presence.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.entities; import java.lang.Comparable; +import java.util.Locale; import eu.siacs.conversations.xml.Element; @@ -32,21 +33,24 @@ public class Presence implements Comparable { this.hash = hash; } - public static Presence parse(Element show, Element caps) { + public static Presence parse(String show, Element caps) { final String hash = caps == null ? null : caps.getAttribute("hash"); final String ver = caps == null ? null : caps.getAttribute("ver"); - if ((show == null) || (show.getContent() == null)) { + if (show == null) { return new Presence(Status.ONLINE, ver, hash); - } else if (show.getContent().equals("away")) { - return new Presence(Status.AWAY, ver, hash); - } else if (show.getContent().equals("xa")) { - return new Presence(Status.XA, ver, hash); - } else if (show.getContent().equals("chat")) { - return new Presence(Status.CHAT, ver, hash); - } else if (show.getContent().equals("dnd")) { - return new Presence(Status.DND, ver, hash); } else { - return new Presence(Status.OFFLINE, ver, hash); + switch (show.toLowerCase(Locale.US)) { + case "away": + return new Presence(Status.AWAY, ver, hash); + case "xa": + return new Presence(Status.XA, ver, hash); + case "dnd": + return new Presence(Status.DND, ver, hash); + case "chat": + return new Presence(Status.CHAT, ver, hash); + default: + return new Presence(Status.ONLINE, ver, hash); + } } } diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index 649f767d..7e2ff213 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -15,6 +15,10 @@ import java.util.TimeZone; import de.thedevstack.conversationsplus.ConversationsPlusApplication; import de.tzur.conversations.Settings; import eu.siacs.conversations.crypto.axolotl.AxolotlService; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.PhoneHelper; +import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; public abstract class AbstractGenerator { private final String[] FEATURES = { @@ -36,10 +40,32 @@ public abstract class AbstractGenerator { "urn:xmpp:chat-markers:0", "urn:xmpp:receipts" }; + private final String[] MESSAGE_CORRECTION_FEATURES = { + "urn:xmpp:message-correct:0" + }; + private String mVersion = null; + protected final String IDENTITY_NAME = "Conversations"; protected final String IDENTITY_TYPE = "phone"; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + protected XmppConnectionService mXmppConnectionService; + + protected AbstractGenerator(XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + protected String getIdentityVersion() { + if (mVersion == null) { + this.mVersion = PhoneHelper.getVersionName(mXmppConnectionService); + } + return this.mVersion; + } + + public String getIdentityName() { + return IDENTITY_NAME + " " + getIdentityVersion(); + } + public String getCapHash() { StringBuilder s = new StringBuilder(); s.append("client/" + IDENTITY_TYPE + "//" + ConversationsPlusApplication.getNameAndVersion() + "<"); @@ -68,6 +94,9 @@ public abstract class AbstractGenerator { if (Settings.CONFIRM_MESSAGE_RECEIVED) { features.addAll(Arrays.asList(MESSAGE_CONFIRMATION_FEATURES)); } + if (mXmppConnectionService.allowMessageCorrection()) { + features.addAll(Arrays.asList(MESSAGE_CORRECTION_FEATURES)); + } Collections.sort(features); return features; } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index a275c01d..0c1f6ee4 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -25,6 +25,7 @@ import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.forms.Data; +import eu.siacs.conversations.xmpp.forms.Field; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.IqPacket; @@ -247,7 +248,7 @@ public class IqGenerator extends AbstractGenerator { public IqPacket requestHttpUploadSlot(Jid host, DownloadableFile file, String mime) { IqPacket packet = new IqPacket(IqPacket.TYPE.GET); packet.setTo(host); - Element request = packet.addChild("request",Xmlns.HTTP_UPLOAD); + Element request = packet.addChild("request", Xmlns.HTTP_UPLOAD); request.addChild("filename").setContent(file.getName()); request.addChild("size").setContent(String.valueOf(file.getExpectedSize())); if (mime != null) { @@ -265,4 +266,31 @@ public class IqGenerator extends AbstractGenerator { return register; } + + public IqPacket pushTokenToAppServer(Jid appServer, String token, String deviceId) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + packet.setTo(appServer); + Element command = packet.addChild("command", "http://jabber.org/protocol/commands"); + command.setAttribute("node","register-push-gcm"); + command.setAttribute("action","execute"); + Data data = new Data(); + data.put("token", token); + data.put("device-id", deviceId); + data.submit(); + command.addChild(data); + return packet; + } + + public IqPacket enablePush(Jid jid, String node, String secret) { + IqPacket packet = new IqPacket(IqPacket.TYPE.SET); + Element enable = packet.addChild("enable","urn:xmpp:push:0"); + enable.setAttribute("jid",jid.toString()); + enable.setAttribute("node", node); + Data data = new Data(); + data.setFormType("http://jabber.org/protocol/pubsub#publish-options"); + data.put("secret",secret); + data.submit(); + enable.addChild(data); + return packet; + } } diff --git a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java index 66ad2a7b..1824a0f8 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -44,6 +44,9 @@ public class MessageGenerator extends AbstractGenerator { } packet.setFrom(account.getJid()); packet.setId(message.getUuid()); + if (message.edited()) { + packet.addChild("replace","urn:xmpp:message-correct:0").setAttribute("id",message.getEditedId()); + } return packet; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 35fed7b6..794badae 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -3,11 +3,13 @@ package eu.siacs.conversations.parser; import android.util.Log; import android.util.Pair; +import eu.siacs.conversations.crypto.PgpDecryptionService; import net.java.otr4j.session.Session; import net.java.otr4j.session.SessionStatus; import java.util.ArrayList; import java.util.Set; +import java.util.UUID; import de.thedevstack.android.logcat.Logging; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; @@ -121,13 +123,6 @@ public class MessageParser extends AbstractParser implements return finishedMessage; } - private Message parsePGPChat(final Conversation conversation, String pgpEncrypted, int status) { - final Message message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); - PgpDecryptionService pgpDecryptionService = conversation.getAccount().getPgpDecryptionService(); - pgpDecryptionService.add(message); - return message; - } - private class Invite { Jid jid; String password; @@ -301,6 +296,8 @@ public class MessageParser extends AbstractParser implements final String body = packet.getBody(); final Element mucUserElement = packet.findChild("x", "http://jabber.org/protocol/muc#user"); final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted"); + final Element replaceElement = packet.findChild("replace","urn:xmpp:message-correct:0"); + final String replacementId = replaceElement == null ? null : replaceElement.getAttribute("id"); final Element axolotlEncrypted = packet.findChild(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX); int status; final Jid counterpart; @@ -314,7 +311,7 @@ public class MessageParser extends AbstractParser implements } boolean isTypeGroupChat = packet.getType() == MessagePacket.TYPE_GROUPCHAT; - boolean isProperlyAddressed = (to != null ) && (!to.isBareJid() || account.countPresences() == 1); + boolean isProperlyAddressed = (to != null ) && (!to.isBareJid() || account.countPresences() <= 1); boolean isMucStatusMessage = from.isBareJid() && mucUserElement != null && mucUserElement.hasChild("status"); if (packet.fromAccount(account)) { status = Message.STATUS_SEND; @@ -338,6 +335,7 @@ public class MessageParser extends AbstractParser implements if (isTypeGroupChat) { if (counterpart.getResourcepart().equals(conversation.getMucOptions().getActualNick())) { status = Message.STATUS_SEND_RECEIVED; + isCarbon = true; //not really carbon but received from another resource if (mXmppConnectionService.markMessage(conversation, remoteMsgId, status)) { return; } else if (remoteMsgId == null || Config.IGNORE_ID_REWRITE_IN_MUC) { @@ -359,10 +357,11 @@ public class MessageParser extends AbstractParser implements return; } } else { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": ignoring OTR message from "+from+" isForwarded="+Boolean.toString(isForwarded)+", isProperlyAddressed="+Boolean.valueOf(isProperlyAddressed)); message = new Message(conversation, body, Message.ENCRYPTION_NONE, status); } } else if (pgpEncrypted != null) { - message = parsePGPChat(conversation, pgpEncrypted, status); + message = new Message(conversation, pgpEncrypted, Message.ENCRYPTION_PGP, status); } else if (axolotlEncrypted != null) { message = parseAxolotlChat(axolotlEncrypted, from, remoteMsgId, conversation, status); if (message == null) { @@ -394,6 +393,42 @@ public class MessageParser extends AbstractParser implements } else { updateLastseen(timestamp, account, packet.getFrom(), true); } + + if (replacementId != null && mXmppConnectionService.allowMessageCorrection()) { + Message replacedMessage = conversation.findMessageWithRemoteIdAndCounterpart(replacementId, + counterpart, + message.getStatus() == Message.STATUS_RECEIVED, + message.isCarbon()); + if (replacedMessage != null) { + final boolean fingerprintsMatch = replacedMessage.getAxolotlFingerprint() == null + || replacedMessage.getAxolotlFingerprint().equals(message.getAxolotlFingerprint()); + final boolean trueCountersMatch = replacedMessage.getTrueCounterpart() != null + && replacedMessage.getTrueCounterpart().equals(message.getTrueCounterpart()); + if (fingerprintsMatch && (trueCountersMatch || conversation.getMode() == Conversation.MODE_SINGLE)) { + Log.d(Config.LOGTAG, "replaced message '" + replacedMessage.getBody() + "' with '" + message.getBody() + "'"); + final String uuid = replacedMessage.getUuid(); + replacedMessage.setUuid(UUID.randomUUID().toString()); + replacedMessage.setBody(message.getBody()); + replacedMessage.setEdited(replacedMessage.getRemoteMsgId()); + replacedMessage.setRemoteMsgId(remoteMsgId); + replacedMessage.setEncryption(message.getEncryption()); + if (replacedMessage.getStatus() == Message.STATUS_RECEIVED) { + replacedMessage.markUnread(); + } + mXmppConnectionService.updateMessage(replacedMessage, uuid); + if (mXmppConnectionService.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { + sendMessageReceipts(account, packet); + } + if (replacedMessage.getEncryption() == Message.ENCRYPTION_PGP) { + conversation.getAccount().getPgpDecryptionService().add(replacedMessage); + } + return; + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received message correction but verification didn't check out"); + } + } + } + boolean checkForDuplicates = query != null || (isTypeGroupChat && packet.hasChild("delay","urn:xmpp:delay")) || message.getType() == Message.TYPE_PRIVATE; @@ -408,6 +443,10 @@ public class MessageParser extends AbstractParser implements conversation.add(message); } + if (message.getEncryption() == Message.ENCRYPTION_PGP) { + conversation.getAccount().getPgpDecryptionService().add(message); + } + if (query == null || query.getWith() == null) { //either no mam or catchup if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) { mXmppConnectionService.markRead(conversation); @@ -424,20 +463,7 @@ public class MessageParser extends AbstractParser implements } if (ConversationsPlusPreferences.confirmMessages() && remoteMsgId != null && !isForwarded && !isTypeGroupChat) { - ArrayList<String> receiptsNamespaces = new ArrayList<>(); - if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { - receiptsNamespaces.add("urn:xmpp:chat-markers:0"); - } - if (packet.hasChild("request", "urn:xmpp:receipts")) { - receiptsNamespaces.add("urn:xmpp:receipts"); - } - if (receiptsNamespaces.size() > 0) { - MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, - packet, - receiptsNamespaces, - packet.getType()); - mXmppConnectionService.sendMessagePacket(account, receipt); - } + sendMessageReceipts(account, packet); } if (message.getStatus() == Message.STATUS_RECEIVED @@ -532,4 +558,21 @@ public class MessageParser extends AbstractParser implements contact.setPresenceName(nick); } } + + private void sendMessageReceipts(Account account, MessagePacket packet) { + ArrayList<String> receiptsNamespaces = new ArrayList<>(); + if (packet.hasChild("markable", "urn:xmpp:chat-markers:0")) { + receiptsNamespaces.add("urn:xmpp:chat-markers:0"); + } + if (packet.hasChild("request", "urn:xmpp:receipts")) { + receiptsNamespaces.add("urn:xmpp:receipts"); + } + if (receiptsNamespaces.size() > 0) { + MessagePacket receipt = mXmppConnectionService.getMessageGenerator().received(account, + packet, + receiptsNamespaces, + packet.getType()); + mXmppConnectionService.sendMessagePacket(account, receipt); + } + } } diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index fcff5c4c..c765dbd0 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -192,7 +192,7 @@ public class PresenceParser extends AbstractParser implements } int sizeBefore = contact.getPresences().size(); - final Element show = packet.findChild("show"); + final String show = packet.findChildContent("show"); final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps"); final Presence presence = Presence.parse(show, caps); contact.updatePresence(resource, presence); diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 040d2f77..96a04fa4 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -52,7 +52,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 23; + private static final int DATABASE_VERSION = 24; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -162,6 +162,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.SERVER_MSG_ID + " TEXT, " + Message.FINGERPRINT + " TEXT, " + Message.CARBON + " INTEGER, " + + Message.EDITED + " TEXT, " + Message.READ + " NUMBER DEFAULT 1, " + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " @@ -371,6 +372,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (oldVersion < 23 && newVersion >= 23) { db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); } + + if (oldVersion < 24 && newVersion >= 24) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + Message.EDITED + " TEXT"); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -587,6 +592,13 @@ public class DatabaseBackend extends SQLiteOpenHelper { + "=?", args); } + public void updateMessage(Message message, String uuid) { + SQLiteDatabase db = this.getWritableDatabase(); + String[] args = {uuid}; + db.update(Message.TABLENAME, message.getContentValues(), Message.UUID + + "=?", args); + } + public void readRoster(Roster roster) { SQLiteDatabase db = this.getReadableDatabase(); Cursor cursor; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 3ad0c514..0ca62b97 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -192,6 +192,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } + public boolean queryInProgress(Conversation conversation) { + return queryInProgress(conversation, null); + } + public void processFin(Element fin, Jid from) { if (fin == null) { return; diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 53b60d5f..9da9ffb2 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -102,6 +102,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; @@ -122,6 +123,7 @@ import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager; import eu.siacs.conversations.xmpp.jingle.OnJinglePacketReceived; import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket; +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; @@ -134,6 +136,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public static final String ACTION_TRY_AGAIN = "try_again"; public static final String ACTION_DISABLE_ACCOUNT = "disable_account"; private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static final String ACTION_GCM_TOKEN_REFRESH = "gcm_token_refresh"; + public static final String ACTION_GCM_MESSAGE_RECEIVED = "gcm_message_received"; private final IBinder mBinder = new XmppConnectionBinder(); private final List<Conversation> conversations = new CopyOnWriteArrayList<>(); private final IqGenerator mIqGenerator = new IqGenerator(); @@ -201,6 +205,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( this); private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); + private PushManagementService mPushManagementService = new PushManagementService(this); private OnConversationUpdate mOnConversationUpdate = null; private final FileObserver fileObserver = new FileObserver( FileBackend.getConversationsImageDirectory()) { @@ -260,6 +265,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa fetchRosterFromServer(account); fetchBookmarks(account); sendPresence(account); + if (mPushManagementService.available(account)) { + mPushManagementService.registerPushTokenOnServer(account); + } mMessageArchiveService.executePendingQueries(account); connectMultiModeConversations(account); syncDirtyContacts(account); @@ -268,7 +276,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private OnStatusChanged statusListener = new OnStatusChanged() { @Override - public void onStatusChanged(Account account) { + public void onStatusChanged(final Account account) { XmppConnection connection = account.getXmppConnection(); if (mOnAccountUpdate != null) { mOnAccountUpdate.onAccountUpdate(); @@ -285,8 +293,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } List<Conversation> conversations = getConversations(); for (Conversation conversation : conversations) { - if (conversation.getAccount() == account && conversation.getMode() == Conversation.MODE_SINGLE) { - conversation.startOtrIfNeeded(); + if (conversation.getAccount() == account + && !account.pendingConferenceJoins.contains(conversation)) { + if (!conversation.startOtrIfNeeded()) { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": couldn't start OTR with "+conversation.getContact().getJid()+" when needed"); + } sendUnsentMessages(conversation); } } @@ -301,7 +312,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa scheduleWakeUpCall(Config.PING_MAX_INTERVAL, account.getUuid().hashCode()); } else if (account.getStatus() == Account.State.OFFLINE) { resetSendingToWaiting(account); - if (!account.isOptionSet(Account.OPTION_DISABLED)) { + final boolean disabled = account.isOptionSet(Account.OPTION_DISABLED); + final boolean pushMode = Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND + && mPushManagementService.available(account) + && checkListeners(); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": push mode "+Boolean.toString(pushMode)); + if (!disabled && !pushMode) { int timeToReconnect = mRandom.nextInt(20) + 10; scheduleWakeUpCall(timeToReconnect, account.getUuid().hashCode()); } @@ -468,6 +484,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa refreshAllPresences(); } break; + case ACTION_GCM_TOKEN_REFRESH: + refreshAllGcmTokens(); + break; + case ACTION_GCM_MESSAGE_RECEIVED: + Log.d(Config.LOGTAG,"gcm push message arrived in service. extras="+intent.getExtras()); } } this.wakeLock.acquire(); @@ -725,18 +746,20 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa disconnect(account, false); } }).start(); - + cancelWakeUpCall(account.getUuid().hashCode()); } } - Context context = getApplicationContext(); - AlarmManager alarmManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); - Intent intent = new Intent(context, EventReceiver.class); - alarmManager.cancel(PendingIntent.getBroadcast(context, 0, intent, 0)); Logging.d(Config.LOGTAG, "good bye"); stopSelf(); } + private void cancelWakeUpCall(int requestCode) { + final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); + final Intent intent = new Intent(this, EventReceiver.class); + intent.setAction("ping"); + alarmManager.cancel(PendingIntent.getBroadcast(this, requestCode, intent, 0)); + } + public void scheduleWakeUpCall(int seconds, int requestCode) { final long timeToWake = SystemClock.elapsedRealtime() + (seconds < 0 ? 1 : seconds + 1) * 1000; @@ -794,8 +817,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa final Conversation conversation = message.getConversation(); account.deactivateGracePeriod(); MessagePacket packet = null; - final boolean addToConversation = conversation.getMode() != Conversation.MODE_MULTI - || account.getServerIdentity() != XmppConnection.Identity.SLACK; + final boolean addToConversation = (conversation.getMode() != Conversation.MODE_MULTI + || account.getServerIdentity() != XmppConnection.Identity.SLACK) + && !message.edited(); boolean saveInDb = addToConversation; message.setStatus(Message.STATUS_WAITING); @@ -852,8 +876,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (message.fixCounterpart()) { conversation.startOtrSession(message.getCounterpart().getResourcepart(), true); } else { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": could not fix counterpart for OTR message to contact "+message.getContact().getJid()); break; } + } else { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+" OTR session with "+message.getContact()+" is in wrong state: "+otrSession.getSessionStatus().toString()); } break; case Message.ENCRYPTION_AXOLOTL: @@ -898,6 +925,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa break; case Message.ENCRYPTION_OTR: if (!conversation.hasValidOtrSession() && message.getCounterpart() != null) { + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": create otr session without starting for "+message.getContact().getJid()); conversation.startOtrSession(message.getCounterpart().getResourcepart(), false); } break; @@ -919,8 +947,12 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (addToConversation) { conversation.add(message); } - if (saveInDb && (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages())) { - databaseBackend.createMessage(message); + if (message.getEncryption() == Message.ENCRYPTION_NONE || saveEncryptedMessages()) { + if (saveInDb) { + databaseBackend.createMessage(message); + } else if (message.edited()) { + databaseBackend.updateMessage(message, message.getEditedId()); + } } updateConversationUi(); } @@ -1695,8 +1727,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa for (Account account : getAccounts()) { if (account.getStatus() == Account.State.ONLINE) { XmppConnection connection = account.getXmppConnection(); - if (connection != null && connection.getFeatures().csi()) { - connection.sendInactive(); + if (connection != null) { + if (connection.getFeatures().csi()) { + connection.sendInactive(); + } + if (Config.CLOSE_TCP_WHEN_SWITCHING_TO_BACKGROUND && mPushManagementService.available(account)) { + connection.waitForPush(); + cancelWakeUpCall(account.getUuid().hashCode()); + } } } } @@ -1708,20 +1746,20 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa List<Conversation> conversations = getConversations(); for (Conversation conversation : conversations) { if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getAccount() == account) { - joinMuc(conversation, true, null); + joinMuc(conversation); } } } public void joinMuc(Conversation conversation) { - joinMuc(conversation, false, null); + joinMuc(conversation, null); } - private void joinMuc(Conversation conversation, boolean now, final OnConferenceJoined onConferenceJoined) { + private void joinMuc(Conversation conversation, final OnConferenceJoined onConferenceJoined) { Account account = conversation.getAccount(); account.pendingConferenceJoins.remove(conversation); account.pendingConferenceLeaves.remove(conversation); - if (account.getStatus() == Account.State.ONLINE || now) { + if (account.getStatus() == Account.State.ONLINE) { conversation.resetMucOptions(); fetchConferenceConfiguration(conversation, new OnConferenceConfigurationFetched() { @@ -1900,7 +1938,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa String name = new BigInteger(75, getRNG()).toString(32); Jid jid = Jid.fromParts(name, server, null); final Conversation conversation = findOrCreateConversation(account, jid, true); - joinMuc(conversation, true, new OnConferenceJoined() { + joinMuc(conversation, new OnConferenceJoined() { @Override public void onConferenceJoined(final Conversation conversation) { Bundle options = new Bundle(); @@ -2110,6 +2148,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa updateConversationUi(); } + public void updateMessage(Message message, String uuid) { + databaseBackend.updateMessage(message, uuid); + updateConversationUi(); + } + protected void syncDirtyContacts(Account account) { for (Contact contact : account.getRoster().getContacts()) { if (contact.getOption(Contact.Options.DIRTY_PUSH)) { @@ -2350,6 +2393,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return getPreferences().getBoolean("confirm_messages", true); } + public boolean allowMessageCorrection() { + return getPreferences().getBoolean("allow_message_correction", true); + } + public boolean sendChatStates() { return getPreferences().getBoolean("chat_states", false); } @@ -2457,7 +2504,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return null; } - public void markRead(final Conversation conversation) { + public boolean markRead(final Conversation conversation) { mNotificationService.clear(conversation); final List<Message> readMessages = conversation.markRead(); if (readMessages.size() > 0) { @@ -2470,8 +2517,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } }; ConversationsPlusApplication.executeDatabaseOperation(runnable); + updateUnreadCountBadge(); + return true; + } else { + return false; } - updateUnreadCountBadge(); } public synchronized void updateUnreadCountBadge() { @@ -2489,7 +2539,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void sendReadMarker(final Conversation conversation) { final Message markable = conversation.getLatestMarkableMessage(); - this.markRead(conversation); + if (this.markRead(conversation)) { + updateConversationUi(); + } if (Settings.CONFIRM_MESSAGE_READ && markable != null && markable.getRemoteMsgId() != null) { Logging.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() + ": sending read marker to " + markable.getCounterpart().toString()); Account account = conversation.getAccount(); @@ -2497,7 +2549,6 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId()); this.sendMessagePacket(conversation.getAccount(), packet); } - updateConversationUi(); } public SecureRandom getRNG() { @@ -2612,6 +2663,14 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } } + private void refreshAllGcmTokens() { + for(Account account : getAccounts()) { + if (account.isOnlineAndConnected() && mPushManagementService.available(account)) { + mPushManagementService.registerPushTokenOnServer(account); + } + } + } + public void sendOfflinePresence(final Account account) { sendPresencePacket(account, mPresenceGenerator.sendOfflinePresence(account)); } @@ -2687,6 +2746,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa * Therefore set this flag to true and try to get messages from server */ conversation.setHasMessagesLeftOnServer(true); + conversation.setLastClearHistory(System.currentTimeMillis()); Runnable runnable = new Runnable() { @Override public void run() { @@ -2776,7 +2836,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa databaseBackend.insertDiscoveryResult(disco); injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco); } else { - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid+" "+presence.getVer()+" vs "+disco.getVer()); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid + " " + presence.getVer() + " vs " + disco.getVer()); } } account.inProgressDiscoFetches.remove(key); @@ -2812,6 +2872,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa }); } + public PushManagementService getPushManagementService() { + return mPushManagementService; + } + public interface OnMamPreferencesFetched { void onPreferencesFetched(Element prefs); void onPreferencesFetchFailed(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index d71958d2..8e25c8ef 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -372,11 +372,7 @@ public class ConversationActivity extends XmppActivity public void sendReadMarkerIfNecessary(final Conversation conversation) { if (!mActivityPaused && conversation != null) { - if (!conversation.isRead()) { - xmppConnectionService.sendReadMarker(conversation); - } else { - xmppConnectionService.markRead(conversation); - } + xmppConnectionService.sendReadMarker(conversation); } } @@ -1581,6 +1577,7 @@ public class ConversationActivity extends XmppActivity public void setMessagesLoaded() { if (mConversationFragment != null) { mConversationFragment.setMessagesLoaded(); + mConversationFragment.updateMessages(); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index ea3d7e71..4257bce3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -46,6 +46,7 @@ import java.util.Collections; import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.UUID; import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import eu.siacs.conversations.Config; @@ -62,6 +63,7 @@ import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.XmppActivity.OnPresenceSelected; import eu.siacs.conversations.ui.XmppActivity.OnValueEdited; @@ -71,6 +73,7 @@ import eu.siacs.conversations.ui.adapter.MessageAdapter.OnContactPictureLongClic import eu.siacs.conversations.ui.listeners.ConversationSwipeRefreshListener; import eu.siacs.conversations.utils.GeoHelper; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.chatstate.ChatState; import eu.siacs.conversations.xmpp.jid.Jid; import github.ankushsachdeva.emojicon.EmojiconGridView; @@ -216,8 +219,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa activity.attachFile(ConversationActivity.ATTACHMENT_CHOICE_CHOOSE_IMAGE); break; case CANCEL: - if (conversation != null && conversation.getMode() == Conversation.MODE_MULTI) { - conversation.setNextCounterpart(null); + if (conversation != null) { + if (conversation.getCorrectingMessage() != null) { + conversation.setCorrectingMessage(null); + mEditMessage.getEditableText().clear(); + } + if (conversation.getMode() == Conversation.MODE_MULTI) { + conversation.setNextCounterpart(null); + } updateChatMsgHint(); updateSendButton(); } @@ -252,12 +261,21 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (body.length() == 0 || this.conversation == null) { return; } - Message message = new Message(conversation, body, conversation.getNextEncryption()); - if (conversation.getMode() == Conversation.MODE_MULTI) { - if (conversation.getNextCounterpart() != null) { - message.setCounterpart(conversation.getNextCounterpart()); - message.setType(Message.TYPE_PRIVATE); + final Message message; + if (conversation.getCorrectingMessage() == null) { + message = new Message(conversation, body, conversation.getNextEncryption()); + if (conversation.getMode() == Conversation.MODE_MULTI) { + if (conversation.getNextCounterpart() != null) { + message.setCounterpart(conversation.getNextCounterpart()); + message.setType(Message.TYPE_PRIVATE); + } } + } else { + message = conversation.getCorrectingMessage(); + message.setBody(body); + message.setEdited(message.getUuid()); + message.setUuid(UUID.randomUUID().toString()); + conversation.setCorrectingMessage(null); } switch (conversation.getNextEncryption()) { case Message.ENCRYPTION_OTR: @@ -278,7 +296,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa public void updateChatMsgHint() { final boolean multi = conversation.getMode() == Conversation.MODE_MULTI; - if (multi && conversation.getNextCounterpart() != null) { + if (conversation.getCorrectingMessage() != null) { + this.mEditMessage.setHint(R.string.send_corrected_message); + } else if (multi && conversation.getNextCounterpart() != null) { this.mEditMessage.setHint(getString( R.string.send_private_message_to, conversation.getNextCounterpart().getResourcepart())); @@ -515,8 +535,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { synchronized (this.messageList) { super.onCreateContextMenu(menu, v, menuInfo); AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; @@ -527,10 +546,15 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private void populateContextMenu(ContextMenu menu) { final Message m = this.selectedMessage; + Message relevantForCorrection = m; + while(relevantForCorrection.mergeable(relevantForCorrection.next())) { + relevantForCorrection = relevantForCorrection.next(); + } if (m.getType() != Message.TYPE_STATUS) { activity.getMenuInflater().inflate(R.menu.message_context, menu); menu.setHeaderTitle(R.string.message_options); MenuItem copyText = menu.findItem(R.id.copy_text); + MenuItem correctMessage = menu.findItem(R.id.correct_message); MenuItem shareWith = menu.findItem(R.id.share_with); MenuItem sendAgain = menu.findItem(R.id.send_again); MenuItem copyUrl = menu.findItem(R.id.copy_url); @@ -542,6 +566,10 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa && m.treatAsDownloadable() != Message.Decision.MUST) { copyText.setVisible(true); } + if (relevantForCorrection.getType() == Message.TYPE_TEXT + && relevantForCorrection.isLastCorrectableMessage()) { + correctMessage.setVisible(true); + } if ((m.getType() != Message.TYPE_TEXT && m.getType() != Message.TYPE_PRIVATE && m.getTransferable() == null) @@ -578,6 +606,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case R.id.copy_text: copyText(selectedMessage); return true; + case R.id.correct_message: + correctMessage(selectedMessage); + return true; case R.id.send_again: resendMessage(selectedMessage); return true; @@ -679,6 +710,16 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateSendButton(); } + private void correctMessage(Message message) { + while(message.mergeable(message.next())) { + message = message.next(); + } + this.conversation.setCorrectingMessage(message); + this.mEditMessage.getEditableText().clear(); + this.mEditMessage.getEditableText().append(message.getBody()); + + } + protected void highlightInConference(String nick) { String oldString = mEditMessage.getText().toString(); if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) { @@ -728,9 +769,6 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.keychainUnlock = KEYCHAIN_UNLOCK_NOT_REQUIRED; this.conversation = conversation; - if (this.conversation.getMode() == Conversation.MODE_MULTI) { - this.conversation.setNextCounterpart(null); - } boolean canWrite = this.conversation.getMode() == Conversation.MODE_SINGLE || this.conversation.getMucOptions().participating(); this.mEditMessage.setEnabled(canWrite); this.mSendButton.setEnabled(canWrite); @@ -985,9 +1023,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa final Conversation c = this.conversation; final SendButtonAction action; final Presence.Status status; - final boolean empty = this.mEditMessage == null || this.mEditMessage.getText().length() == 0; + final String text = this.mEditMessage == null ? "" : this.mEditMessage.getText().toString(); + final boolean empty = text.length() == 0; final boolean conference = c.getMode() == Conversation.MODE_MULTI; - if (conference && !c.getAccount().httpUploadAvailable()) { + if (c.getCorrectingMessage() != null && (empty || text.equals(c.getCorrectingMessage().getBody()))) { + action = SendButtonAction.CANCEL; + } else if (conference && !c.getAccount().httpUploadAvailable()) { if (empty && c.getNextCounterpart() != null) { action = SendButtonAction.CANCEL; } else { @@ -1040,9 +1081,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa this.mSendButton.setImageResource(getSendButtonImageResource(action, status)); } - public void updateStatusMessages() { + protected void updateStatusMessages() { synchronized (this.messageList) { - if (conversation.getLastClearHistory() != 0) { + if (showLoadMoreMessages(conversation)) { this.messageList.add(0, Message.createLoadMoreMessage(conversation)); } if (conversation.getMode() == Conversation.MODE_SINGLE) { @@ -1068,8 +1109,14 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } } - protected void showSnackbar(final int message, final int action, - final OnClickListener clickListener) { + private boolean showLoadMoreMessages(final Conversation c) { + final XmppConnection connection = c.getAccount().getXmppConnection(); + final boolean mam = connection != null && connection.getFeatures().mam(); + final MessageArchiveService service = activity.xmppConnectionService.getMessageArchiveService(); + return mam && (c.getLastClearHistory() != 0 || (c.countMessages() == 0 && c.hasMessagesLeftOnServer() && !service.queryInProgress(c))); + } + + protected void showSnackbar(final int message, final int action, final OnClickListener clickListener) { snackbar.setVisibility(View.VISIBLE); snackbar.setOnClickListener(null); snackbarMessage.setText(message); @@ -1262,6 +1309,13 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa updateSendButton(); } + @Override + public void onTextChanged() { + if (conversation != null && conversation.getCorrectingMessage() != null) { + updateSendButton(); + } + } + private int completionIndex = 0; private int lastCompletionLength = 0; private String incomplete; diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index db3a451b..2b89ea05 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -29,6 +29,7 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TableLayout; +import android.widget.TableRow; import android.widget.TextView; import android.widget.Toast; @@ -51,6 +52,7 @@ import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnKeyStatusUpdated; +import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jid.InvalidJidException; @@ -79,6 +81,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private TextView mServerInfoBlocking; private TextView mServerInfoPep; private TextView mServerInfoHttpUpload; + private TextView mServerInfoPush; private TextView mSessionEst; private TextView mOtrFingerprint; private TextView mAxolotlFingerprint; @@ -225,6 +228,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } }; private Toast mFetchingMamPrefsToast; + private TableRow mPushRow; public void refreshUiReal() { invalidateOptionsMenu(); @@ -303,15 +307,14 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate @Override public void run() { final Intent intent; - if (avatar != null) { - intent = new Intent(getApplicationContext(), - StartConversationActivity.class); + final XmppConnection connection = mAccount.getXmppConnection(); + if (avatar != null || (connection != null && !connection.getFeatures().pep())) { + intent = new Intent(getApplicationContext(), StartConversationActivity.class); if (xmppConnectionService != null && xmppConnectionService.getAccounts().size() == 1) { intent.putExtra("init", true); } } else { - intent = new Intent(getApplicationContext(), - PublishProfilePictureActivity.class); + intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); intent.putExtra(EXTRA_ACCOUNT, mAccount.getJid().toBareJid().toString()); intent.putExtra("setup", true); } @@ -424,6 +427,8 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); this.mServerInfoHttpUpload = (TextView) findViewById(R.id.server_info_http_upload); + this.mPushRow = (TableRow) findViewById(R.id.push_row); + this.mServerInfoPush = (TextView) findViewById(R.id.server_info_push); this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); this.mOtrFingerprintBox = (RelativeLayout) findViewById(R.id.otr_fingerprint_box); this.mOtrFingerprintToClipboardButton = (ImageButton) findViewById(R.id.action_copy_to_clipboard); @@ -691,6 +696,14 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } else { this.mServerInfoHttpUpload.setText(R.string.server_info_unavailable); } + + this.mPushRow.setVisibility(xmppConnectionService.getPushManagementService().isStub() ? View.GONE : View.VISIBLE); + + if (xmppConnectionService.getPushManagementService().available(mAccount)) { + this.mServerInfoPush.setText(R.string.server_info_available); + } else { + this.mServerInfoPush.setText(R.string.server_info_unavailable); + } final String otrFingerprint = this.mAccount.getOtrFingerprint(); if (otrFingerprint != null) { this.mOtrFingerprintBox.setVisibility(View.VISIBLE); @@ -916,7 +929,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate mFetchingMamPrefsToast.cancel(); } AlertDialog.Builder builder = new Builder(EditAccountActivity.this); - builder.setTitle(R.string.mam_prefs); + builder.setTitle(R.string.server_side_mam_prefs); String defaultAttr = prefs.getAttribute("default"); final List<String> defaults = Arrays.asList("never", "roster", "always"); final AtomicInteger choice = new AtomicInteger(Math.max(0,defaults.indexOf(defaultAttr))); diff --git a/src/main/java/eu/siacs/conversations/ui/EditMessage.java b/src/main/java/eu/siacs/conversations/ui/EditMessage.java index 06868a98..8cc59cde 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditMessage.java +++ b/src/main/java/eu/siacs/conversations/ui/EditMessage.java @@ -69,6 +69,7 @@ public class EditMessage extends EmojiconEditText { this.isUserTyping = false; this.keyboardListener.onTextDeleted(); } + this.keyboardListener.onTextChanged(); } } @@ -84,6 +85,7 @@ public class EditMessage extends EmojiconEditText { void onTypingStarted(); void onTypingStopped(); void onTextDeleted(); + void onTextChanged(); boolean onTabPressed(boolean repeated); } diff --git a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java index 83ab83e7..49deab38 100644 --- a/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SettingsActivity.java @@ -165,7 +165,8 @@ public class SettingsActivity extends XmppActivity implements xmppConnectionService.toggleForegroundService(); } else if (name.equals("confirm_messages") || name.equals("xa_on_silent_mode") - || name.equals("away_when_screen_off")) { + || name.equals("away_when_screen_off") + || name.equals("allow_message_correction")) { if (xmppConnectionServiceBound) { if (name.equals("away_when_screen_off")) { xmppConnectionService.toggleScreenEventReceiver(); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 78c8f67c..99b7d49d 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -124,6 +124,16 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (viewHolder.indicatorReceived != null) { viewHolder.indicatorReceived.setVisibility(View.GONE); } + + if (viewHolder.edit_indicator != null) { + if (message.edited()) { + viewHolder.edit_indicator.setVisibility(View.VISIBLE); + viewHolder.edit_indicator.setImageResource(darkBackground ? R.drawable.ic_mode_edit_white_18dp : R.drawable.ic_mode_edit_black_18dp); + viewHolder.edit_indicator.setAlpha(darkBackground ? 0.7f : 0.57f); + } else { + viewHolder.edit_indicator.setVisibility(View.GONE); + } + } boolean multiReceived = message.getConversation().getMode() == Conversation.MODE_MULTI && message.getMergedStatus() <= Message.STATUS_RECEIVED; if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.getTransferable() != null) { @@ -180,7 +190,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { if (message.getEncryption() == Message.ENCRYPTION_NONE) { viewHolder.indicator.setVisibility(View.GONE); } else { - viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_secure_indicator_white : R.drawable.ic_secure_indicator); + viewHolder.indicator.setImageResource(darkBackground ? R.drawable.ic_lock_white_18dp : R.drawable.ic_lock_black_18dp); viewHolder.indicator.setVisibility(View.VISIBLE); if (message.getEncryption() == Message.ENCRYPTION_AXOLOTL) { XmppAxolotlSession.Trust trust = message.getConversation() @@ -466,6 +476,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { .findViewById(R.id.download_button); viewHolder.indicator = (ImageView) view .findViewById(R.id.security_indicator); + viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator); viewHolder.image = (ImageView) view .findViewById(R.id.message_image); viewHolder.messageBody = (TextView) view @@ -486,6 +497,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { .findViewById(R.id.download_button); viewHolder.indicator = (ImageView) view .findViewById(R.id.security_indicator); + viewHolder.edit_indicator = (ImageView) view.findViewById(R.id.edit_indicator); viewHolder.image = (ImageView) view .findViewById(R.id.message_image); viewHolder.messageBody = (TextView) view @@ -691,6 +703,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { protected TextView status_message; protected TextView encryption; public Button load_more_messages; + public ImageView edit_indicator; } class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 6cef8d8c..2653e5f3 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -2,6 +2,8 @@ package eu.siacs.conversations.xmpp; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.os.Bundle; +import android.os.Parcelable; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.os.SystemClock; @@ -20,11 +22,12 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigInteger; import java.net.ConnectException; +import java.net.IDN; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.URL; import java.net.UnknownHostException; +import java.net.URL; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.Principal; @@ -336,13 +339,7 @@ public class XmppConnection implements Runnable { this.changeStatus(Account.State.OFFLINE); this.attempt--; //don't count attempt when reconnecting instantly anyway } finally { - if (socket != null) { - try { - socket.close(); - } catch (IOException e) { - - } - } + forceCloseSocket(); if (wakeLock.isHeld()) { try { wakeLock.release(); @@ -414,13 +411,7 @@ public class XmppConnection implements Runnable { @Override public void run() { - try { - if (socket != null) { - socket.close(); - } - } catch (final IOException ignored) { - - } + forceCloseSocket(); connect(); } @@ -1074,10 +1065,10 @@ public class XmppConnection implements Runnable { if (mPendingServiceDiscoveries == 0) { Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": done with service discovery"); Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": online with resource " + account.getResource()); - changeStatus(Account.State.ONLINE); if (bindListener != null) { bindListener.onBind(account); } + changeStatus(Account.State.ONLINE); } } } @@ -1267,14 +1258,45 @@ public class XmppConnection implements Runnable { } } - public void disconnect(final boolean force) { - Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force)); - if (force) { + public void waitForPush() { + if (tagWriter.isActive()) { + tagWriter.finish(); + new Thread(new Runnable() { + @Override + public void run() { + try { + while(!tagWriter.finished()) { + Thread.sleep(10); + } + socket.close(); + Logging.d(Config.LOGTAG,account.getJid().toBareJid()+": closed tcp without closing stream"); + } catch (IOException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }).start(); + } else { + forceCloseSocket(); + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": closed tcp without closing stream (no waiting)"); + } + } + + private void forceCloseSocket() { + if (socket != null) { try { socket.close(); - } catch(Exception e) { + } catch (IOException e) { Logging.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": exception during force close ("+e.getMessage()+")"); } + } + } + + public void disconnect(final boolean force) { + Logging.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting force="+Boolean.valueOf(force)); + if (force) { + forceCloseSocket(); return; } else { if (tagWriter.isActive()) { @@ -1484,17 +1506,13 @@ public class XmppConnection implements Runnable { } public boolean mam() { - if (hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:mam:0")) { - return true; - } else { - return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0"); - } + return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:mam:0") + || hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0"); } - public boolean advancedStreamFeaturesLoaded() { - synchronized (XmppConnection.this.disco) { - return disco.containsKey(account.getServer()); - } + public boolean push() { + return hasDiscoFeature(account.getJid().toBareJid(), "urn:xmpp:push:0") + || hasDiscoFeature(account.getServer(), "urn:xmpp:push:0"); } public boolean rosterVersioning() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java index 50a41892..380f0280 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -80,8 +80,13 @@ public class Data extends Element { } public String getFormType() { - Field typeFiled = this.getFieldByName("FORM_TYPE"); - return typeFiled == null ? "" : typeFiled.getValue(); + String type = getValue("FORM_TYPE"); + return type == null ? "" : type; + } + + public String getValue(String name) { + Field field = this.getFieldByName(name); + return field == null ? null : field.getValue(); } public String getTitle() { |