diff options
Diffstat (limited to 'src/main/java/eu')
63 files changed, 3729 insertions, 2191 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 8b46335a..ad874660 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -20,10 +20,18 @@ public final class Config { public static final boolean PARSE_EMOTICONS = false; public static final boolean UTF8_EMOTICONS = false; + + public static final int PAGE_SIZE = 50; + public static final int MAX_NUM_PAGES = 3; + public static final int PROGRESS_UI_UPDATE_INTERVAL = 750; public static final boolean NO_PROXY_LOOKUP = false; //useful to debug ibb + public static final long MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000; + public static final long MAM_MAX_CATCHUP = MILLISECONDS_IN_DAY / 2; + public static final int MAM_MAX_MESSAGES = 500; + private Config() { } diff --git a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java index 642d0ed0..7f7b350c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java +++ b/src/main/java/eu/siacs/conversations/crypto/OtrEngine.java @@ -34,7 +34,7 @@ import net.java.otr4j.crypto.OtrCryptoException; import net.java.otr4j.session.InstanceTag; import net.java.otr4j.session.SessionID; -public class OtrEngine implements OtrEngineHost { +public class OtrEngine extends OtrCryptoEngineImpl implements OtrEngineHost { private Account account; private OtrPolicy otrPolicy; @@ -126,7 +126,7 @@ public class OtrEngine implements OtrEngineHost { @Override public byte[] getLocalFingerprintRaw(SessionID arg0) { try { - return new OtrCryptoEngineImpl().getFingerprintRaw(getPublicKey()); + return getFingerprintRaw(getPublicKey()); } catch (OtrCryptoException e) { return null; } @@ -180,6 +180,7 @@ public class OtrEngine implements OtrEngineHost { packet.setBody(body); packet.addChild("private", "urn:xmpp:carbons:2"); packet.addChild("no-copy", "urn:xmpp:hints"); + packet.addChild("no-store", "urn:xmpp:hints"); packet.setType(MessagePacket.TYPE_CHAT); account.getXmppConnection().sendMessagePacket(packet); } @@ -257,10 +258,10 @@ public class OtrEngine implements OtrEngineHost { Conversation conversation = this.mXmppConnectionService.find(this.account,jid); if (conversation!=null) { if (approved) { - conversation.getContact().addOtrFingerprint(CryptoHelper.prettifyFingerprint(fingerprint)); + conversation.getContact().addOtrFingerprint(fingerprint); } conversation.smp().hint = null; - conversation.smp().status = Conversation.Smp.STATUS_FINISHED; + conversation.smp().status = Conversation.Smp.STATUS_VERIFIED; mXmppConnectionService.updateConversationUi(); mXmppConnectionService.syncRosterToDisk(conversation.getAccount()); } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java index c7dedc5e..40a55151 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -14,7 +14,7 @@ public class Plain extends SaslMechanism { @Override public int getPriority() { - return 0; + return 10; } @Override diff --git a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java index 92b8a729..957b0a14 100644 --- a/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java +++ b/src/main/java/eu/siacs/conversations/entities/AbstractEntity.java @@ -17,5 +17,4 @@ public abstract class AbstractEntity { public boolean equals(AbstractEntity entity) { return this.getUuid().equals(entity.getUuid()); } - } diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 538d0ec2..b0cde62c 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -10,9 +10,12 @@ import net.java.otr4j.crypto.OtrCryptoException; import org.json.JSONException; import org.json.JSONObject; +import java.security.PublicKey; import java.security.interfaces.DSAPublicKey; +import java.util.Collection; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; @@ -56,7 +59,7 @@ public class Account extends AbstractEntity { SECURITY_ERROR(true), INCOMPATIBLE_SERVER(true); - private boolean isError; + private final boolean isError; public boolean isError() { return this.isError; @@ -116,11 +119,11 @@ public class Account extends AbstractEntity { protected boolean online = false; private OtrEngine otrEngine = null; private XmppConnection xmppConnection = null; - private Presences presences = new Presences(); private long mEndGracePeriod = 0L; private String otrFingerprint; - private Roster roster = null; + private final Roster roster = new Roster(this); private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); + private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>(); public Account() { this.uuid = "0"; @@ -150,7 +153,7 @@ public class Account extends AbstractEntity { this.avatar = avatar; } - public static Account fromCursor(Cursor cursor) { + public static Account fromCursor(final Cursor cursor) { Jid jid = null; try { jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)), @@ -166,11 +169,11 @@ public class Account extends AbstractEntity { cursor.getString(cursor.getColumnIndex(AVATAR))); } - public boolean isOptionSet(int option) { + public boolean isOptionSet(final int option) { return ((options & (1 << option)) != 0); } - public void setOption(int option, boolean value) { + public void setOption(final int option, final boolean value) { if (value) { this.options |= 1 << option; } else { @@ -241,34 +244,18 @@ public class Account extends AbstractEntity { return keys; } - public String getSSLFingerprint() { - if (keys.has("ssl_cert")) { - try { - return keys.getString("ssl_cert"); - } catch (JSONException e) { - return null; - } - } else { - return null; - } - } - - public void setSSLCertFingerprint(String fingerprint) { - this.setKey("ssl_cert", fingerprint); - } - - public boolean setKey(String keyName, String keyValue) { + public boolean setKey(final String keyName, final String keyValue) { try { this.keys.put(keyName, keyValue); return true; - } catch (JSONException e) { + } catch (final JSONException e) { return false; } } @Override public ContentValues getContentValues() { - ContentValues values = new ContentValues(); + final ContentValues values = new ContentValues(); values.put(UUID, uuid); values.put(USERNAME, jid.getLocalpart()); values.put(SERVER, jid.getDomainpart()); @@ -280,7 +267,7 @@ public class Account extends AbstractEntity { return values; } - public void initOtrEngine(XmppConnectionService context) { + public void initOtrEngine(final XmppConnectionService context) { this.otrEngine = new OtrEngine(context, this); } @@ -292,7 +279,7 @@ public class Account extends AbstractEntity { return this.xmppConnection; } - public void setXmppConnection(XmppConnection connection) { + public void setXmppConnection(final XmppConnection connection) { this.xmppConnection = connection; } @@ -302,8 +289,8 @@ public class Account extends AbstractEntity { if (this.otrEngine == null) { return null; } - DSAPublicKey publicKey = (DSAPublicKey) this.otrEngine.getPublicKey(); - if (publicKey == null) { + final PublicKey publicKey = this.otrEngine.getPublicKey(); + if (publicKey == null || !(publicKey instanceof DSAPublicKey)) { return null; } this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey); @@ -324,31 +311,19 @@ public class Account extends AbstractEntity { } } - public void setRosterVersion(String version) { + public void setRosterVersion(final String version) { this.rosterVersion = version; } - public void updatePresence(String resource, int status) { - this.presences.updatePresence(resource, status); - } - - public void removePresence(String resource) { - this.presences.removePresence(resource); - } - - public void clearPresences() { - this.presences = new Presences(); - } - public int countPresences() { - return this.presences.size(); + return this.getRoster().getContact(this.getJid().toBareJid()).getPresences().size(); } public String getPgpSignature() { if (keys.has("pgp_signature")) { try { return keys.getString("pgp_signature"); - } catch (JSONException e) { + } catch (final JSONException e) { return null; } } else { @@ -357,9 +332,6 @@ public class Account extends AbstractEntity { } public Roster getRoster() { - if (this.roster == null) { - this.roster = new Roster(this); - } return this.roster; } @@ -367,12 +339,12 @@ public class Account extends AbstractEntity { return this.bookmarks; } - public void setBookmarks(List<Bookmark> bookmarks) { + public void setBookmarks(final List<Bookmark> bookmarks) { this.bookmarks = bookmarks; } public boolean hasBookmarkFor(final Jid conferenceJid) { - for (Bookmark bookmark : this.bookmarks) { + for (final Bookmark bookmark : this.bookmarks) { final Jid jid = bookmark.getJid(); if (jid != null && jid.equals(conferenceJid.toBareJid())) { return true; @@ -381,7 +353,7 @@ public class Account extends AbstractEntity { return false; } - public boolean setAvatar(String filename) { + public boolean setAvatar(final String filename) { if (this.avatar != null && this.avatar.equals(filename)) { return false; } else { @@ -408,11 +380,32 @@ public class Account extends AbstractEntity { } public String getShareableUri() { - String fingerprint = this.getOtrFingerprint(); + final String fingerprint = this.getOtrFingerprint(); if (fingerprint != null) { return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint; } else { return "xmpp:" + this.getJid().toBareJid().toString(); } } + + public boolean isBlocked(final ListItem contact) { + final Jid jid = contact.getJid(); + return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid())); + } + + public boolean isBlocked(final Jid jid) { + return jid != null && blocklist.contains(jid.toBareJid()); + } + + public Collection<Jid> getBlocklist() { + return this.blocklist; + } + + public void clearBlocklist() { + getBlocklist().clear(); + } + + public boolean isOnlineAndConnected() { + return this.getStatus() == State.ONLINE && this.getXmppConnection() != null; + } } diff --git a/src/main/java/eu/siacs/conversations/entities/Blockable.java b/src/main/java/eu/siacs/conversations/entities/Blockable.java new file mode 100644 index 00000000..dbcd55c4 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Blockable.java @@ -0,0 +1,11 @@ +package eu.siacs.conversations.entities; + +import eu.siacs.conversations.xmpp.jid.Jid; + +public interface Blockable { + public boolean isBlocked(); + public boolean isDomainBlocked(); + public Jid getBlockedJid(); + public Jid getJid(); + public Account getAccount(); +} diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 559e2f2d..f81f1a87 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/src/main/java/eu/siacs/conversations/entities/Bookmark.java @@ -6,7 +6,6 @@ import java.util.Locale; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; public class Bookmark extends Element implements ListItem { @@ -60,16 +59,7 @@ public class Bookmark extends Element implements ListItem { @Override public Jid getJid() { - final String jid = this.getAttribute("jid"); - if (jid != null) { - try { - return Jid.fromString(jid); - } catch (final InvalidJidException e) { - return null; - } - } else { - return null; - } + return this.getAttributeAsJid("jid"); } @Override @@ -102,9 +92,7 @@ public class Bookmark extends Element implements ListItem { } public boolean autojoin() { - String autojoin = this.getAttribute("autojoin"); - return (autojoin != null && (autojoin.equalsIgnoreCase("true") || autojoin - .equalsIgnoreCase("1"))); + return this.getAttributeAsBoolean("autojoin"); } public String getPassword() { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 6a6b41d6..698e0322 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -16,7 +16,7 @@ import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -public class Contact implements ListItem { +public class Contact implements ListItem, Blockable { public static final String TABLENAME = "contacts"; public static final String SYSTEMNAME = "systemname"; @@ -47,8 +47,8 @@ public class Contact implements ListItem { protected Account account; public Contact(final String account, final String systemName, final String serverName, - final Jid jid, final int subscription, final String photoUri, - final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) { + final Jid jid, final int subscription, final String photoUri, + final String systemAccount, final String keys, final String avatar, final Lastseen lastseen, final String groups) { this.accountUuid = account; this.systemName = systemName; this.serverName = serverName; @@ -122,11 +122,10 @@ public class Contact implements ListItem { @Override public List<Tag> getTags() { - ArrayList<Tag> tags = new ArrayList<Tag>(); - for (String group : getGroups()) { + final ArrayList<Tag> tags = new ArrayList<>(); + for (final String group : getGroups()) { tags.add(new Tag(group, UIHelper.getColorForName(group))); } - int status = getMostAvailableStatus(); switch (getMostAvailableStatus()) { case Presences.CHAT: case Presences.ONLINE: @@ -142,6 +141,9 @@ public class Contact implements ListItem { tags.add(new Tag("dnd", 0xffe51c23)); break; } + if (isBlocked()) { + tags.add(new Tag("blocked", 0xff2e2f3b)); + } return tags; } @@ -176,7 +178,7 @@ public class Contact implements ListItem { } public ContentValues getContentValues() { - ContentValues values = new ContentValues(); + final ContentValues values = new ContentValues(); values.put(ACCOUNT, accountUuid); values.put(SYSTEMNAME, systemName); values.put(SERVERNAME, serverName); @@ -213,11 +215,11 @@ public class Contact implements ListItem { this.presences = pres; } - public void updatePresence(String resource, int status) { + public void updatePresence(final String resource, final int status) { this.presences.updatePresence(resource, status); } - public void removePresence(String resource) { + public void removePresence(final String resource) { this.presences.removePresence(resource); } @@ -266,13 +268,15 @@ public class Contact implements ListItem { } public ArrayList<String> getOtrFingerprints() { - ArrayList<String> fingerprints = new ArrayList<String>(); + final ArrayList<String> fingerprints = new ArrayList<String>(); try { if (this.keys.has("otr_fingerprints")) { - JSONArray prints = this.keys - .getJSONArray("otr_fingerprints"); + final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); for (int i = 0; i < prints.length(); ++i) { - fingerprints.add(prints.getString(i)); + final String print = prints.isNull(i) ? null : prints.getString(i); + if (print != null && !print.isEmpty()) { + fingerprints.add(prints.getString(i)); + } } } } catch (final JSONException ignored) { @@ -335,8 +339,8 @@ public class Contact implements ListItem { public boolean showInRoster() { return (this.getOption(Contact.Options.IN_ROSTER) && (!this - .getOption(Contact.Options.DIRTY_DELETE))) - || (this.getOption(Contact.Options.DIRTY_PUSH)); + .getOption(Contact.Options.DIRTY_DELETE))) + || (this.getOption(Contact.Options.DIRTY_PUSH)); } public void parseSubscriptionFromElement(Element item) { @@ -426,7 +430,7 @@ public class Contact implements ListItem { if (this.keys.has("otr_fingerprints")) { JSONArray newPrints = new JSONArray(); JSONArray oldPrints = this.keys - .getJSONArray("otr_fingerprints"); + .getJSONArray("otr_fingerprints"); for (int i = 0; i < oldPrints.length(); ++i) { if (!oldPrints.getString(i).equals(fingerprint)) { newPrints.put(oldPrints.getString(i)); @@ -449,28 +453,46 @@ public class Contact implements ListItem { public String getShareableUri() { if (getOtrFingerprints().size() >= 1) { String otr = getOtrFingerprints().get(0); - return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr.replace(" ", ""); + return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr; } else { return "xmpp:" + getJid().toBareJid().toString(); } } + @Override + public boolean isBlocked() { + return getAccount().isBlocked(this); + } + + @Override + public boolean isDomainBlocked() { + return getAccount().isBlocked(this.getJid().toDomainJid()); + } + + @Override + public Jid getBlockedJid() { + if (isDomainBlocked()) { + return getJid().toDomainJid(); + } else { + return getJid(); + } + } + public static class Lastseen { public long time; public String presence; public Lastseen() { - time = 0; - presence = null; + this(null, 0); } public Lastseen(final String presence, final long time) { - this.time = time; this.presence = presence; + this.time = time; } } - public class Options { + public final class Options { public static final int TO = 0; public static final int FROM = 1; public static final int ASKING = 2; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index a7da0bc2..470bd290 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -16,12 +16,15 @@ import org.json.JSONObject; import java.security.interfaces.DSAPublicKey; import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -public class Conversation extends AbstractEntity { +public class Conversation extends AbstractEntity implements Blockable { public static final String TABLENAME = "conversations"; public static final int STATUS_AVAILABLE = 0; @@ -43,6 +46,7 @@ public class Conversation extends AbstractEntity { public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption"; public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password"; public static final String ATTRIBUTE_MUTED_TILL = "muted_till"; + public static final String ATTRIBUTE_LAST_MESSAGE_TRANSMITTED = "last_message_transmitted"; private String name; private String contactUuid; @@ -72,6 +76,140 @@ public class Conversation extends AbstractEntity { private Bookmark bookmark; + private boolean messagesLeftOnServer = true; + + public boolean hasMessagesLeftOnServer() { + return messagesLeftOnServer; + } + + public void setHasMessagesLeftOnServer(boolean value) { + this.messagesLeftOnServer = value; + } + + public Message findUnsentMessageWithUuid(String uuid) { + synchronized(this.messages) { + for (final Message message : this.messages) { + final int s = message.getStatus(); + if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) { + return message; + } + } + } + return null; + } + + public void findWaitingMessages(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for(Message message : this.messages) { + if (message.getStatus() == Message.STATUS_WAITING) { + onMessageFound.onMessageFound(message); + } + } + } + } + + public void findMessagesWithFiles(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (Message message : this.messages) { + if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) + && message.getEncryption() != Message.ENCRYPTION_PGP) { + onMessageFound.onMessageFound(message); + } + } + } + } + + public Message findMessageWithFileAndUuid(String uuid) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (message.getType() == Message.TYPE_IMAGE + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { + return message; + } + } + } + return null; + } + + public void clearMessages() { + synchronized (this.messages) { + this.messages.clear(); + } + } + + public void trim() { + synchronized (this.messages) { + final int size = messages.size(); + final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES; + if (size > maxsize) { + this.messages.subList(0, size - maxsize).clear(); + } + } + } + + public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (Message message : this.messages) { + if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) + && (message.getEncryption() == Message.ENCRYPTION_OTR)) { + onMessageFound.onMessageFound(message); + } + } + } + } + + public void findUnsentTextMessages(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (message.getType() != Message.TYPE_IMAGE + && message.getStatus() == Message.STATUS_UNSEND) { + onMessageFound.onMessageFound(message); + } + } + } + } + + public Message findSentMessageWithUuid(String uuid) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (uuid.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND && uuid + .equals(message.getRemoteMsgId()))) { + return message; + } + } + } + return null; + } + + public void populateWithMessages(final List<Message> messages) { + synchronized (this.messages) { + messages.clear(); + messages.addAll(this.messages); + } + } + + @Override + public boolean isBlocked() { + return getContact().isBlocked(); + } + + @Override + public boolean isDomainBlocked() { + return getContact().isDomainBlocked(); + } + + @Override + public Jid getBlockedJid() { + return getContact().getBlockedJid(); + } + + + public interface OnMessageFound { + public void onMessageFound(final Message message); + } + public Conversation(final String name, final Account account, final Jid contactJid, final int mode) { this(java.util.UUID.randomUUID().toString(), name, null, account @@ -98,18 +236,11 @@ public class Conversation extends AbstractEntity { } } - public List<Message> getMessages() { - return messages; - } - public boolean isRead() { - return (this.messages == null) || (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); - } + return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead(); + } public void markRead() { - if (this.messages == null) { - return; - } for (int i = this.messages.size() - 1; i >= 0; --i) { if (messages.get(i).isRead()) { break; @@ -119,9 +250,6 @@ public class Conversation extends AbstractEntity { } public Message getLatestMarkableMessage() { - if (this.messages == null) { - return null; - } for (int i = this.messages.size() - 1; i >= 0; --i) { if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED && this.messages.get(i).markable) { @@ -130,13 +258,13 @@ public class Conversation extends AbstractEntity { } else { return this.messages.get(i); } - } + } } return null; } public Message getLatestMessage() { - if ((this.messages == null) || (this.messages.size() == 0)) { + if (this.messages.size() == 0) { Message message = new Message(this, "", Message.ENCRYPTION_NONE); message.setTime(getCreated()); return message; @@ -158,7 +286,7 @@ public class Conversation extends AbstractEntity { if (generatedName != null) { return generatedName; } else { - return getContactJid().getLocalpart(); + return getJid().getLocalpart(); } } } else { @@ -166,10 +294,6 @@ public class Conversation extends AbstractEntity { } } - public String getProfilePhotoString() { - return this.getContact().getProfilePhoto(); - } - public String getAccountUuid() { return this.accountUuid; } @@ -182,11 +306,12 @@ public class Conversation extends AbstractEntity { return this.account.getRoster().getContact(this.contactJid); } - public void setAccount(Account account) { + public void setAccount(final Account account) { this.account = account; } - public Jid getContactJid() { + @Override + public Jid getJid() { return this.contactJid; } @@ -213,14 +338,14 @@ public class Conversation extends AbstractEntity { } public static Conversation fromCursor(Cursor cursor) { - Jid jid; - try { - jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID))); - } catch (final InvalidJidException e) { - // Borked DB.. - jid = null; - } - return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), + Jid jid; + try { + jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID))); + } catch (final InvalidJidException e) { + // Borked DB.. + jid = null; + } + return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)), cursor.getString(cursor.getColumnIndex(NAME)), cursor.getString(cursor.getColumnIndex(CONTACT)), cursor.getString(cursor.getColumnIndex(ACCOUNT)), @@ -247,9 +372,9 @@ public class Conversation extends AbstractEntity { if (this.otrSession != null) { return this.otrSession; } else { - final SessionID sessionId = new SessionID(this.getContactJid().toBareJid().toString(), - presence, - "xmpp"); + final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(), + presence, + "xmpp"); this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine()); try { if (sendStart) { @@ -288,7 +413,7 @@ public class Conversation extends AbstractEntity { } catch (OtrException e) { this.resetOtrSession(); } - } + } } public boolean endOtrIfNeeded() { @@ -315,30 +440,29 @@ public class Conversation extends AbstractEntity { return this.otrSession != null; } - public String getOtrFingerprint() { + public synchronized String getOtrFingerprint() { if (this.otrFingerprint == null) { try { - if (getOtrSession() == null) { - return ""; + if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { + return null; } - DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession() - .getRemotePublicKey(); - StringBuilder builder = new StringBuilder( - new OtrCryptoEngineImpl().getFingerprint(remotePubKey)); - builder.insert(8, " "); - builder.insert(17, " "); - builder.insert(26, " "); - builder.insert(35, " "); - this.otrFingerprint = builder.toString(); - } catch (final OtrCryptoException ignored) { - + DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey(); + this.otrFingerprint = getAccount().getOtrEngine().getFingerprint(remotePubKey); + } catch (final OtrCryptoException | UnsupportedOperationException ignored) { + return null; } } return this.otrFingerprint; } - public void verifyOtrFingerprint() { - getContact().addOtrFingerprint(getOtrFingerprint()); + public boolean verifyOtrFingerprint() { + final String fingerprint = getOtrFingerprint(); + if (fingerprint != null) { + getContact().addOtrFingerprint(fingerprint); + return true; + } else { + return false; + } } public boolean isOtrFingerprintVerified() { @@ -450,9 +574,11 @@ public class Conversation extends AbstractEntity { } public boolean hasDuplicateMessage(Message message) { - for (int i = this.getMessages().size() - 1; i >= 0; --i) { - if (this.messages.get(i).equals(message)) { - return true; + synchronized (this.messages) { + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).equals(message)) { + return true; + } } } return false; @@ -460,7 +586,7 @@ public class Conversation extends AbstractEntity { public Message findSentMessageWithBody(String body) { synchronized (this.messages) { - for (int i = this.getMessages().size() - 1; i >= 0; --i) { + for (int i = this.messages.size() - 1; i >= 0; --i) { Message message = this.messages.get(i); if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) && message.getBody() != null && message.getBody().equals(body)) { return message; @@ -470,6 +596,31 @@ public class Conversation extends AbstractEntity { } } + public boolean setLastMessageTransmitted(long value) { + long before = getLastMessageTransmitted(); + if (value - before > 1000) { + this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value)); + return true; + } else { + return false; + } + } + + public long getLastMessageTransmitted() { + long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0); + if (timestamp == 0) { + synchronized (this.messages) { + for(int i = this.messages.size() - 1; i >= 0; --i) { + Message message = this.messages.get(i); + if (message.getStatus() == Message.STATUS_RECEIVED) { + return message.getTimeSent(); + } + } + } + } + return timestamp; + } + public void setMutedTill(long value) { this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value)); } @@ -535,12 +686,32 @@ public class Conversation extends AbstractEntity { } } + public void sort() { + synchronized (this.messages) { + Collections.sort(this.messages, new Comparator<Message>() { + @Override + public int compare(Message left, Message right) { + if (left.getTimeSent() < right.getTimeSent()) { + return -1; + } else if (left.getTimeSent() > right.getTimeSent()) { + return 1; + } else { + return 0; + } + } + }); + for(Message message : this.messages) { + message.untie(); + } + } + } + public class Smp { public static final int STATUS_NONE = 0; public static final int STATUS_CONTACT_REQUESTED = 1; public static final int STATUS_WE_REQUESTED = 2; public static final int STATUS_FAILED = 3; - public static final int STATUS_FINISHED = 4; + public static final int STATUS_VERIFIED = 4; public String secret = null; public String hint = null; diff --git a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java index 25f33907..7c8f95d1 100644 --- a/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java +++ b/src/main/java/eu/siacs/conversations/entities/DownloadableFile.java @@ -56,12 +56,16 @@ public class DownloadableFile extends File { public String getMimeType() { String path = this.getAbsolutePath(); - String mime = URLConnection.guessContentTypeFromName(path); - if (mime != null) { - return mime; - } else if (mime == null && path.endsWith(".webp")) { - return "image/webp"; - } else { + try { + String mime = URLConnection.guessContentTypeFromName(path.replace("#","")); + if (mime != null) { + return mime; + } else if (mime == null && path.endsWith(".webp")) { + return "image/webp"; + } else { + return ""; + } + } catch (final StringIndexOutOfBoundsException e) { return ""; } } diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java index db9fbc37..efc1c2b9 100644 --- a/src/main/java/eu/siacs/conversations/entities/ListItem.java +++ b/src/main/java/eu/siacs/conversations/entities/ListItem.java @@ -12,10 +12,10 @@ public interface ListItem extends Comparable<ListItem> { public List<Tag> getTags(); public final class Tag { - private String name; - private int color; + private final String name; + private final int color; - public Tag(String name, int color) { + public Tag(final String name, final int color) { this.name = name; this.color = color; } @@ -28,4 +28,6 @@ public interface ListItem extends Comparable<ListItem> { return this.name; } } + + public boolean match(final String needle); } diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index 47861d06..b5a1897d 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -45,6 +45,7 @@ public class Message extends AbstractEntity { public static String STATUS = "status"; public static String TYPE = "type"; public static String REMOTE_MSG_ID = "remoteMsgId"; + public static String SERVER_MSG_ID = "serverMsgId"; public static String RELATIVE_FILE_PATH = "relativeFilePath"; public boolean markable = false; protected String conversationUuid; @@ -59,6 +60,7 @@ public class Message extends AbstractEntity { protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; + protected String serverMsgId = null; protected Conversation conversation = null; protected Downloadable downloadable = null; private Message mNextMessage = null; @@ -75,7 +77,7 @@ public class Message extends AbstractEntity { public Message(Conversation conversation, String body, int encryption, int status) { this(java.util.UUID.randomUUID().toString(), conversation.getUuid(), - conversation.getContactJid() == null ? null : conversation.getContactJid().toBareJid(), + conversation.getJid() == null ? null : conversation.getJid().toBareJid(), null, body, System.currentTimeMillis(), @@ -83,13 +85,15 @@ public class Message extends AbstractEntity { status, TYPE_TEXT, null, + null, null); this.conversation = conversation; } private Message(final String uuid, final String conversationUUid, final Jid counterpart, - final Jid trueCounterpart, final String body, final long timeSent, - final int encryption, final int status, final int type, final String remoteMsgId, final String relativeFilePath) { + final Jid trueCounterpart, final String body, final long timeSent, + final int encryption, final int status, final int type, final String remoteMsgId, + final String relativeFilePath, final String serverMsgId) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -101,6 +105,7 @@ public class Message extends AbstractEntity { this.type = type; this.remoteMsgId = remoteMsgId; this.relativeFilePath = relativeFilePath; + this.serverMsgId = serverMsgId; } public static Message fromCursor(Cursor cursor) { @@ -136,7 +141,8 @@ public class Message extends AbstractEntity { cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(TYPE)), cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), - cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH))); + cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); } public static Message createStatusMessage(Conversation conversation) { @@ -168,6 +174,7 @@ public class Message extends AbstractEntity { values.put(TYPE, type); values.put(REMOTE_MSG_ID, remoteMsgId); values.put(RELATIVE_FILE_PATH, relativeFilePath); + values.put(SERVER_MSG_ID,serverMsgId); return values; } @@ -199,7 +206,7 @@ public class Message extends AbstractEntity { return null; } else { return this.conversation.getAccount().getRoster() - .getContactFromRoster(this.trueCounterpart); + .getContactFromRoster(this.trueCounterpart); } } } @@ -248,6 +255,14 @@ public class Message extends AbstractEntity { this.remoteMsgId = id; } + public String getServerMsgId() { + return this.serverMsgId; + } + + public void setServerMsgId(String id) { + this.serverMsgId = id; + } + public boolean isRead() { return this.read; } @@ -293,38 +308,43 @@ public class Message extends AbstractEntity { } public boolean equals(Message message) { - return (this.remoteMsgId != null) && (this.body != null) && (this.counterpart != null) && this.remoteMsgId.equals(message.getRemoteMsgId()) && this.body.equals(message.getBody()) && this.counterpart.equals(message.getCounterpart()); + if (this.serverMsgId != null && message.getServerMsgId() != null) { + return this.serverMsgId.equals(message.getServerMsgId()); + } else { + return this.body != null + && this.counterpart != null + && ((this.remoteMsgId != null && this.remoteMsgId.equals(message.getRemoteMsgId())) + || this.uuid.equals(message.getRemoteMsgId())) && this.body.equals(message.getBody()) + && this.counterpart.equals(message.getCounterpart()); + } } public Message next() { - if (this.mNextMessage == null) { - synchronized (this.conversation.messages) { + synchronized (this.conversation.messages) { + if (this.mNextMessage == null) { int index = this.conversation.messages.indexOf(this); - if (index < 0 - || index >= this.conversation.getMessages().size() - 1) { + if (index < 0 || index >= this.conversation.messages.size() - 1) { this.mNextMessage = null; } else { - this.mNextMessage = this.conversation.messages - .get(index + 1); + this.mNextMessage = this.conversation.messages.get(index + 1); } } + return this.mNextMessage; } - return this.mNextMessage; } public Message prev() { - if (this.mPreviousMessage == null) { - synchronized (this.conversation.messages) { + synchronized (this.conversation.messages) { + if (this.mPreviousMessage == null) { int index = this.conversation.messages.indexOf(this); if (index <= 0 || index > this.conversation.messages.size()) { this.mPreviousMessage = null; } else { - this.mPreviousMessage = this.conversation.messages - .get(index - 1); + this.mPreviousMessage = this.conversation.messages.get(index - 1); } } + return this.mPreviousMessage; } - return this.mPreviousMessage; } public boolean mergeable(final Message message) { @@ -368,7 +388,7 @@ public class Message extends AbstractEntity { if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) { return false; - } + } if (url.getPath() == null) { return false; } @@ -382,14 +402,14 @@ public class Message extends AbstractEntity { String[] extensionParts = filename.split("\\."); if (extensionParts.length == 2 && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains( - extensionParts[extensionParts.length - 1])) { + extensionParts[extensionParts.length - 1])) { return true; } else if (extensionParts.length == 3 && Arrays .asList(Downloadable.VALID_CRYPTO_EXTENSIONS) .contains(extensionParts[extensionParts.length - 1]) && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains( - extensionParts[extensionParts.length - 2])) { + extensionParts[extensionParts.length - 2])) { return true; } else { return false; @@ -493,6 +513,15 @@ public class Message extends AbstractEntity { } } + public void untie() { + this.mNextMessage = null; + this.mPreviousMessage = null; + } + + public boolean isFileOrImage() { + return type == TYPE_FILE || type == TYPE_IMAGE; + } + public class ImageParams { public URL url; public long size = 0; diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index c8706fc9..97a63532 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -80,20 +80,20 @@ public class MucOptions { public void setRole(String role) { role = role.toLowerCase(); - switch (role) { - case "moderator": - this.role = ROLE_MODERATOR; - break; - case "participant": - this.role = ROLE_PARTICIPANT; - break; - case "visitor": - this.role = ROLE_VISITOR; - break; - default: - this.role = ROLE_NONE; - break; - } + switch (role) { + case "moderator": + this.role = ROLE_MODERATOR; + break; + case "participant": + this.role = ROLE_PARTICIPANT; + break; + case "visitor": + this.role = ROLE_VISITOR; + break; + default: + this.role = ROLE_NONE; + break; + } } public int getAffiliation() { @@ -164,7 +164,7 @@ public class MucOptions { } public void processPacket(PresencePacket packet, PgpEngine pgp) { - final Jid from = packet.getFrom(); + final Jid from = packet.getFrom(); if (!from.isBareJid()) { final String name = from.getResourcepart(); final String type = packet.getAttribute("type"); @@ -179,7 +179,7 @@ public class MucOptions { user.setAffiliation(item.getAttribute("affiliation")); user.setRole(item.getAttribute("role")); user.setJid(item.getAttributeAsJid("jid")); - if (codes.contains(STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(this.conversation.getContactJid())) { + if (codes.contains(STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(this.conversation.getJid())) { this.isOnline = true; this.error = ERROR_NO_ERROR; self = user; @@ -204,14 +204,14 @@ public class MucOptions { msg = ""; } user.setPgpKeyId(pgp.fetchKeyId(account, msg, - signed.getContent())); + signed.getContent())); } } } } } else if (type.equals("unavailable")) { if (codes.contains(STATUS_CODE_SELF_PRESENCE) || - packet.getFrom().equals(this.conversation.getContactJid())) { + packet.getFrom().equals(this.conversation.getJid())) { if (codes.contains(STATUS_CODE_CHANGED_NICK)) { this.mNickChangingInProgress = true; } else if (codes.contains(STATUS_CODE_KICKED)) { @@ -282,8 +282,8 @@ public class MucOptions { && conversation.getBookmark().getNick() != null && !conversation.getBookmark().getNick().isEmpty()) { return conversation.getBookmark().getNick(); - } else if (!conversation.getContactJid().isBareJid()) { - return conversation.getContactJid().getResourcepart(); + } else if (!conversation.getJid().isBareJid()) { + return conversation.getJid().getResourcepart(); } else { return account.getUsername(); } @@ -334,14 +334,14 @@ public class MucOptions { public String createNameFromParticipants() { if (users.size() >= 2) { List<String> names = new ArrayList<String>(); - for (User user : users) { - Contact contact = user.getContact(); - if (contact != null && !contact.getDisplayName().isEmpty()) { - names.add(contact.getDisplayName().split("\\s+")[0]); - } else { - names.add(user.getName()); - } + for (User user : users) { + Contact contact = user.getContact(); + if (contact != null && !contact.getDisplayName().isEmpty()) { + names.add(contact.getDisplayName().split("\\s+")[0]); + } else { + names.add(user.getName()); } + } StringBuilder builder = new StringBuilder(); for (int i = 0; i < names.size(); ++i) { builder.append(names.get(i)); @@ -388,12 +388,12 @@ public class MucOptions { } public Jid createJoinJid(String nick) { - try { - return Jid.fromString(this.conversation.getContactJid().toBareJid().toString() + "/"+nick); - } catch (final InvalidJidException e) { - return null; - } - } + try { + return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/"+nick); + } catch (final InvalidJidException e) { + return null; + } + } public Jid getTrueCounterpart(String counterpart) { for (User user : this.getUsers()) { diff --git a/src/main/java/eu/siacs/conversations/entities/Roster.java b/src/main/java/eu/siacs/conversations/entities/Roster.java index 12a89cec..1a81a419 100644 --- a/src/main/java/eu/siacs/conversations/entities/Roster.java +++ b/src/main/java/eu/siacs/conversations/entities/Roster.java @@ -7,7 +7,7 @@ import java.util.concurrent.ConcurrentHashMap; import eu.siacs.conversations.xmpp.jid.Jid; public class Roster { - Account account; + final Account account; final ConcurrentHashMap<String, Contact> contacts = new ConcurrentHashMap<>(); private String version = null; @@ -19,7 +19,7 @@ public class Roster { if (jid == null) { return null; } - Contact contact = contacts.get(jid.toBareJid().toString()); + final Contact contact = contacts.get(jid.toBareJid().toString()); if (contact != null && contact.showInRoster()) { return contact; } else { @@ -32,7 +32,7 @@ public class Roster { if (contacts.containsKey(bareJid.toString())) { return contacts.get(bareJid.toString()); } else { - Contact contact = new Contact(bareJid); + final Contact contact = new Contact(bareJid); contact.setAccount(account); contacts.put(bareJid.toString(), contact); return contact; @@ -46,13 +46,13 @@ public class Roster { } public void markAllAsNotInRoster() { - for (Contact contact : getContacts()) { + for (final Contact contact : getContacts()) { contact.resetOption(Contact.Options.IN_ROSTER); } } public void clearSystemAccounts() { - for (Contact contact : getContacts()) { + for (final Contact contact : getContacts()) { contact.setPhotoUri(null); contact.setSystemName(null); contact.setSystemAccount(null); diff --git a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java index f46e7ba4..870ee757 100644 --- a/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/AbstractGenerator.java @@ -4,9 +4,12 @@ import android.util.Base64; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Locale; +import java.util.TimeZone; import eu.siacs.conversations.services.XmppConnectionService; @@ -20,9 +23,11 @@ public abstract class AbstractGenerator { "http://jabber.org/protocol/disco#info", "urn:xmpp:avatar:metadata+notify", "urn:xmpp:ping"}; - public final String IDENTITY_NAME = "Conversations 0.9.3"; + public final String IDENTITY_NAME = "Conversations 0.10"; public final String IDENTITY_TYPE = "phone"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); + protected XmppConnectionService mXmppConnectionService; protected AbstractGenerator(XmppConnectionService service) { @@ -46,4 +51,9 @@ public abstract class AbstractGenerator { byte[] sha1 = md.digest(s.toString().getBytes()); return new String(Base64.encode(sha1, Base64.DEFAULT)).trim(); } + + public static String getTimestamp(long time) { + DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); + return DATE_FORMAT.format(time); + } } diff --git a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java index 5d674748..f94dc5d7 100644 --- a/src/main/java/eu/siacs/conversations/generator/IqGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/IqGenerator.java @@ -4,52 +4,56 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class IqGenerator extends AbstractGenerator { - public IqGenerator(XmppConnectionService service) { + public IqGenerator(final XmppConnectionService service) { super(service); } - public IqPacket discoResponse(IqPacket request) { - IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT); + public IqPacket discoResponse(final IqPacket request) { + final IqPacket packet = new IqPacket(IqPacket.TYPE_RESULT); packet.setId(request.getId()); - packet.setTo(request.getFrom()); - Element query = packet.addChild("query", + packet.setTo(request.getFrom()); + final Element query = packet.addChild("query", "http://jabber.org/protocol/disco#info"); query.setAttribute("node", request.query().getAttribute("node")); - Element identity = query.addChild("identity"); + final Element identity = query.addChild("identity"); identity.setAttribute("category", "client"); identity.setAttribute("type", this.IDENTITY_TYPE); identity.setAttribute("name", IDENTITY_NAME); - List<String> features = Arrays.asList(FEATURES); + final List<String> features = Arrays.asList(FEATURES); Collections.sort(features); - for (String feature : features) { + for (final String feature : features) { query.addChild("feature").setAttribute("var", feature); } return packet; } - protected IqPacket publish(String node, Element item) { - IqPacket packet = new IqPacket(IqPacket.TYPE_SET); - Element pubsub = packet.addChild("pubsub", + protected IqPacket publish(final String node, final Element item) { + final IqPacket packet = new IqPacket(IqPacket.TYPE_SET); + final Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub"); - Element publish = pubsub.addChild("publish"); + final Element publish = pubsub.addChild("publish"); publish.setAttribute("node", node); publish.addChild(item); return packet; } protected IqPacket retrieve(String node, Element item) { - IqPacket packet = new IqPacket(IqPacket.TYPE_GET); - Element pubsub = packet.addChild("pubsub", + final IqPacket packet = new IqPacket(IqPacket.TYPE_GET); + final Element pubsub = packet.addChild("pubsub", "http://jabber.org/protocol/pubsub"); - Element items = pubsub.addChild("items"); + final Element items = pubsub.addChild("items"); items.setAttribute("node", node); if (item != null) { items.addChild(item); @@ -58,19 +62,19 @@ public class IqGenerator extends AbstractGenerator { } public IqPacket publishAvatar(Avatar avatar) { - Element item = new Element("item"); + final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - Element data = item.addChild("data", "urn:xmpp:avatar:data"); + final Element data = item.addChild("data", "urn:xmpp:avatar:data"); data.setContent(avatar.image); return publish("urn:xmpp:avatar:data", item); } - public IqPacket publishAvatarMetadata(Avatar avatar) { - Element item = new Element("item"); + public IqPacket publishAvatarMetadata(final Avatar avatar) { + final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - Element metadata = item - .addChild("metadata", "urn:xmpp:avatar:metadata"); - Element info = metadata.addChild("info"); + final Element metadata = item + .addChild("metadata", "urn:xmpp:avatar:metadata"); + final Element info = metadata.addChild("info"); info.setAttribute("bytes", avatar.size); info.setAttribute("id", avatar.sha1sum); info.setAttribute("height", avatar.height); @@ -79,10 +83,10 @@ public class IqGenerator extends AbstractGenerator { return publish("urn:xmpp:avatar:metadata", item); } - public IqPacket retrieveAvatar(Avatar avatar) { - Element item = new Element("item"); + public IqPacket retrieveAvatar(final Avatar avatar) { + final Element item = new Element("item"); item.setAttribute("id", avatar.sha1sum); - IqPacket packet = retrieve("urn:xmpp:avatar:data", item); + final IqPacket packet = retrieve("urn:xmpp:avatar:data", item); packet.setTo(avatar.owner); return packet; } @@ -94,4 +98,54 @@ public class IqGenerator extends AbstractGenerator { } return packet; } + + public IqPacket queryMessageArchiveManagement(final MessageArchiveService.Query mam) { + final IqPacket packet = new IqPacket(IqPacket.TYPE_SET); + final Element query = packet.query("urn:xmpp:mam:0"); + query.setAttribute("queryid",mam.getQueryId()); + final Data data = new Data(); + data.setFormType("urn:xmpp:mam:0"); + if (mam.getWith()!=null) { + data.put("with", mam.getWith().toString()); + } + data.put("start",getTimestamp(mam.getStart())); + data.put("end",getTimestamp(mam.getEnd())); + query.addChild(data); + if (mam.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { + query.addChild("set", "http://jabber.org/protocol/rsm").addChild("before").setContent(mam.getReference()); + } else if (mam.getReference() != null) { + query.addChild("set", "http://jabber.org/protocol/rsm").addChild("after").setContent(mam.getReference()); + } + return packet; + } + public IqPacket generateGetBlockList() { + final IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + iq.addChild("blocklist", Xmlns.BLOCKING); + + return iq; + } + + public IqPacket generateSetBlockRequest(final Jid jid) { + final IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + final Element block = iq.addChild("block", Xmlns.BLOCKING); + block.addChild("item").setAttribute("jid", jid.toBareJid().toString()); + return iq; + } + + public IqPacket generateSetUnblockRequest(final Jid jid) { + final IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + final Element block = iq.addChild("unblock", Xmlns.BLOCKING); + block.addChild("item").setAttribute("jid", jid.toBareJid().toString()); + return iq; + } + + public IqPacket generateSetPassword(final Account account, final String newPassword) { + final IqPacket packet = new IqPacket(IqPacket.TYPE_SET); + packet.setTo(account.getServer()); + final Element query = packet.addChild("query", Xmlns.REGISTER); + final Jid jid = account.getJid(); + query.addChild("username").setContent(jid.getLocalpart()); + query.addChild("password").setContent(newPassword); + 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 ca5417e0..8e99888b 100644 --- a/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/MessageGenerator.java @@ -114,7 +114,7 @@ public class MessageGenerator extends AbstractGenerator { private MessagePacket generateError(MessagePacket origin) { MessagePacket packet = new MessagePacket(); packet.setId(origin.getId()); - packet.setTo(origin.getFrom()); + packet.setTo(origin.getFrom()); packet.setBody(origin.getBody()); packet.setType(MessagePacket.TYPE_ERROR); return packet; @@ -135,7 +135,7 @@ public class MessageGenerator extends AbstractGenerator { String subject) { MessagePacket packet = new MessagePacket(); packet.setType(MessagePacket.TYPE_GROUPCHAT); - packet.setTo(conversation.getContactJid().toBareJid()); + packet.setTo(conversation.getJid().toBareJid()); Element subjectChild = new Element("subject"); subjectChild.setContent(subject); packet.addChild(subjectChild); @@ -149,13 +149,13 @@ public class MessageGenerator extends AbstractGenerator { packet.setTo(contact); packet.setFrom(conversation.getAccount().getJid()); Element x = packet.addChild("x", "jabber:x:conference"); - x.setAttribute("jid", conversation.getContactJid().toBareJid().toString()); + x.setAttribute("jid", conversation.getJid().toBareJid().toString()); return packet; } public MessagePacket invite(Conversation conversation, Jid contact) { MessagePacket packet = new MessagePacket(); - packet.setTo(conversation.getContactJid().toBareJid()); + packet.setTo(conversation.getJid().toBareJid()); packet.setFrom(conversation.getAccount().getJid()); Element x = new Element("x"); x.setAttribute("xmlns", "http://jabber.org/protocol/muc#user"); @@ -170,7 +170,7 @@ public class MessageGenerator extends AbstractGenerator { MessagePacket originalMessage, String namespace) { MessagePacket receivedPacket = new MessagePacket(); receivedPacket.setType(MessagePacket.TYPE_NORMAL); - receivedPacket.setTo(originalMessage.getFrom()); + receivedPacket.setTo(originalMessage.getFrom()); receivedPacket.setFrom(account.getJid()); Element received = receivedPacket.addChild("received", namespace); received.setAttribute("id", originalMessage.getId()); diff --git a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java index eedfca16..08070c08 100644 --- a/src/main/java/eu/siacs/conversations/parser/AbstractParser.java +++ b/src/main/java/eu/siacs/conversations/parser/AbstractParser.java @@ -2,8 +2,6 @@ package eu.siacs.conversations.parser; import java.text.ParseException; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Collections; import java.util.Date; import java.util.Locale; @@ -11,7 +9,6 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; public abstract class AbstractParser { @@ -24,54 +21,39 @@ public abstract class AbstractParser { protected long getTimestamp(Element packet) { long now = System.currentTimeMillis(); - ArrayList<String> stamps = new ArrayList<>(); - for (Element child : packet.getChildren()) { - if (child.getName().equals("delay")) { - stamps.add(child.getAttribute("stamp").replace("Z", "+0000")); - } + Element delay = packet.findChild("delay"); + if (delay == null) { + return now; } - Collections.sort(stamps); - if (stamps.size() >= 1) { - try { - String stamp = stamps.get(stamps.size() - 1); - if (stamp.contains(".")) { - Date date = new SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US) - .parse(stamp); - if (now < date.getTime()) { - return now; - } else { - return date.getTime(); - } - } else { - Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", - Locale.US).parse(stamp); - if (now < date.getTime()) { - return now; - } else { - return date.getTime(); - } - } - } catch (ParseException e) { - return now; - } - } else { + 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; } } + public static Date parseTimestamp(String timestamp) throws ParseException { + timestamp = timestamp.replace("Z", "+0000"); + SimpleDateFormat dateFormat; + if (timestamp.contains(".")) { + dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ", Locale.US); + } else { + dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ",Locale.US); + } + return dateFormat.parse(timestamp); + } + protected void updateLastseen(final Element packet, final Account account, final boolean presenceOverwrite) { - Jid from; - try { - from = Jid.fromString(packet.getAttribute("from")).toBareJid(); - } catch (final InvalidJidException e) { - // TODO: Handle this? - from = null; - } - String presence = from == null || from.isBareJid() ? "" : from.getResourcepart(); - Contact contact = account.getRoster().getContact(from); - long timestamp = getTimestamp(packet); + final Jid from = packet.getAttributeAsJid("from"); + final String presence = from == null || from.isBareJid() ? "" : from.getResourcepart(); + final Contact contact = account.getRoster().getContact(from); + final long timestamp = getTimestamp(packet); if (timestamp >= contact.lastseen.time) { contact.lastseen.time = timestamp; if (!presence.isEmpty() && presenceOverwrite) { diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index aeec56d0..e84545fc 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -2,36 +2,40 @@ package eu.siacs.conversations.parser; import android.util.Log; +import java.util.ArrayList; +import java.util.Collection; + import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnIqPacketReceived; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class IqParser extends AbstractParser implements OnIqPacketReceived { - public IqParser(XmppConnectionService service) { + public IqParser(final XmppConnectionService service) { super(service); } - public void rosterItems(Account account, Element query) { - String version = query.getAttribute("ver"); + private void rosterItems(final Account account, final Element query) { + final String version = query.getAttribute("ver"); if (version != null) { account.getRoster().setVersion(version); } - for (Element item : query.getChildren()) { + for (final Element item : query.getChildren()) { if (item.getName().equals("item")) { final Jid jid = item.getAttributeAsJid("jid"); if (jid == null) { - break; + continue; } - String name = item.getAttribute("name"); - String subscription = item.getAttribute("subscription"); - Contact contact = account.getRoster().getContact(jid); + final String name = item.getAttribute("name"); + final String subscription = item.getAttribute("subscription"); + final Contact contact = account.getRoster().getContact(jid); if (!contact.getOption(Contact.Options.DIRTY_PUSH)) { contact.setServerName(name); contact.parseGroupsFromElement(item); @@ -54,53 +58,104 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { mXmppConnectionService.updateRosterUi(); } - public String avatarData(IqPacket packet) { - Element pubsub = packet.findChild("pubsub", + public String avatarData(final IqPacket packet) { + final Element pubsub = packet.findChild("pubsub", "http://jabber.org/protocol/pubsub"); if (pubsub == null) { return null; } - Element items = pubsub.findChild("items"); + final Element items = pubsub.findChild("items"); if (items == null) { return null; } return super.avatarData(items); } + public static boolean fromServer(final Account account, final IqPacket packet) { + return packet.getFrom() == null + || packet.getFrom().equals(account.getServer()) + || packet.getFrom().equals(account.getJid().toBareJid()) + || packet.getFrom().equals(account.getJid()); + } + @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.hasChild("query", "jabber:iq:roster")) { - final Jid from = packet.getFrom(); - if ((from == null) || (from.equals(account.getJid().toBareJid()))) { - Element query = packet.findChild("query"); - this.rosterItems(account, query); + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.hasChild("query", Xmlns.ROSTER) && fromServer(account, packet)) { + final Element query = packet.findChild("query"); + // If this is in response to a query for the whole roster: + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.getRoster().markAllAsNotInRoster(); } - } else { - if (packet.getFrom() == null) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": received iq with invalid from "+packet.toString()); - return; - } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") - || packet.hasChild("data", "http://jabber.org/protocol/ibb")) { - mXmppConnectionService.getJingleConnectionManager() - .deliverIbbPacket(account, packet); - } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { - IqPacket response = mXmppConnectionService.getIqGenerator() - .discoResponse(packet); - account.getXmppConnection().sendIqPacket(response, null); - } else if (packet.hasChild("ping", "urn:xmpp:ping")) { - IqPacket response = packet.generateRespone(IqPacket.TYPE_RESULT); - mXmppConnectionService.sendIqPacket(account, response, null); + this.rosterItems(account, query); + } else if ((packet.hasChild("block", Xmlns.BLOCKING) || packet.hasChild("blocklist", Xmlns.BLOCKING)) && + fromServer(account, packet)) { + // Block list or block push. + Log.d(Config.LOGTAG, "Received blocklist update from server"); + final Element blocklist = packet.findChild("blocklist", Xmlns.BLOCKING); + final Element block = packet.findChild("block", Xmlns.BLOCKING); + final Collection<Element> items = blocklist != null ? blocklist.getChildren() : + (block != null ? block.getChildren() : null); + // If this is a response to a blocklist query, clear the block list and replace with the new one. + // Otherwise, just update the existing blocklist. + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.clearBlocklist(); + } + if (items != null) { + final Collection<Jid> jids = new ArrayList<>(items.size()); + // Create a collection of Jids from the packet + for (final Element item : items) { + if (item.getName().equals("item")) { + final Jid jid = item.getAttributeAsJid("jid"); + if (jid != null) { + jids.add(jid); + } + } + } + account.getBlocklist().addAll(jids); + } + // Update the UI + mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + } else if (packet.hasChild("unblock", Xmlns.BLOCKING) && + fromServer(account, packet) && packet.getType() == IqPacket.TYPE_SET) { + Log.d(Config.LOGTAG, "Received unblock update from server"); + final Collection<Element> items = packet.findChild("unblock", Xmlns.BLOCKING).getChildren(); + if (items.size() == 0) { + // No children to unblock == unblock all + account.getBlocklist().clear(); } else { - if ((packet.getType() == IqPacket.TYPE_GET) - || (packet.getType() == IqPacket.TYPE_SET)) { - IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); - Element error = response.addChild("error"); - error.setAttribute("type", "cancel"); - error.addChild("feature-not-implemented", - "urn:ietf:params:xml:ns:xmpp-stanzas"); - account.getXmppConnection().sendIqPacket(response, null); + final Collection<Jid> jids = new ArrayList<>(items.size()); + for (final Element item : items) { + if (item.getName().equals("item")) { + final Jid jid = item.getAttributeAsJid("jid"); + if (jid != null) { + jids.add(jid); + } + } } + account.getBlocklist().removeAll(jids); } + mXmppConnectionService.updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); + } else if (packet.hasChild("open", "http://jabber.org/protocol/ibb") + || packet.hasChild("data", "http://jabber.org/protocol/ibb")) { + mXmppConnectionService.getJingleConnectionManager() + .deliverIbbPacket(account, packet); + } else if (packet.hasChild("query", "http://jabber.org/protocol/disco#info")) { + final IqPacket response = mXmppConnectionService.getIqGenerator() + .discoResponse(packet); + account.getXmppConnection().sendIqPacket(response, null); + } else if (packet.hasChild("ping", "urn:xmpp:ping")) { + final IqPacket response = packet.generateResponse(IqPacket.TYPE_RESULT); + mXmppConnectionService.sendIqPacket(account, response, null); + } else { + if ((packet.getType() == IqPacket.TYPE_GET) + || (packet.getType() == IqPacket.TYPE_SET)) { + final IqPacket response = packet.generateResponse(IqPacket.TYPE_ERROR); + final Element error = response.addChild("error"); + error.setAttribute("type", "cancel"); + error.addChild("feature-not-implemented", + "urn:ietf:params:xml:ns:xmpp-stanzas"); + account.getXmppConnection().sendIqPacket(response, null); + } } } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 782675da..44cda261 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -10,11 +10,11 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.services.MessageArchiveService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnMessagePacketReceived; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; @@ -60,12 +60,12 @@ public class MessageParser extends AbstractParser implements } private Message parseOtrChat(MessagePacket packet, Account account) { - boolean properlyAddressed = (!packet.getTo().isBareJid()) - || (account.countPresences() == 1); - final Jid from = packet.getFrom(); - if (from == null) { + 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); String presence; @@ -124,10 +124,6 @@ public class MessageParser extends AbstractParser implements finishedMessage.setCounterpart(from); return finishedMessage; } catch (Exception e) { - String receivedId = packet.getId(); - if (receivedId != null) { - mXmppConnectionService.replyWithNotAcceptable(account, packet); - } conversation.resetOtrSession(); return null; } @@ -230,7 +226,7 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.getConversations(), account, to.toBareJid()); if (conversation != null) { - mXmppConnectionService.markRead(conversation, false); + mXmppConnectionService.markRead(conversation); } } } @@ -277,6 +273,69 @@ public class MessageParser extends AbstractParser implements 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(), @@ -444,12 +503,22 @@ public class MessageParser extends AbstractParser implements if (message != null) { if (message.getStatus() == Message.STATUS_SEND) { account.activateGracePeriod(); - mXmppConnectionService.markRead( - message.getConversation(), false); + 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); + } + 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); } @@ -459,8 +528,7 @@ public class MessageParser extends AbstractParser implements if (message.getStatus() == Message.STATUS_RECEIVED) { message.markUnread(); } else { - mXmppConnectionService.markRead(message.getConversation(), - false); + mXmppConnectionService.markRead(message.getConversation()); account.activateGracePeriod(); } } @@ -491,12 +559,16 @@ public class MessageParser extends AbstractParser implements } Conversation conversation = message.getConversation(); conversation.add(message); + if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().advancedStreamFeaturesLoaded()) { + if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) { + mXmppConnectionService.updateConversation(conversation); + } + } if (message.getStatus() == Message.STATUS_RECEIVED && conversation.getOtrSession() != null && !conversation.getOtrSession().getSessionID().getUserID() .equals(message.getCounterpart().getResourcepart())) { - Log.d(Config.LOGTAG, "ending because of reasons"); conversation.endOtrIfNeeded(); } @@ -509,7 +581,7 @@ public class MessageParser extends AbstractParser implements if (message.trusted() && message.bodyContainsDownloadable()) { this.mXmppConnectionService.getHttpConnectionManager() .createNewConnection(message); - } else { + } else if (!message.isRead()) { mXmppConnectionService.getNotificationService().push(message); } mXmppConnectionService.updateConversationUi(); diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 43c8fa8d..accb56ea 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -45,69 +45,56 @@ public class PresenceParser extends AbstractParser implements } final Jid from = packet.getFrom(); String type = packet.getAttribute("type"); - if (from.toBareJid().equals(account.getJid().toBareJid())) { + Contact contact = account.getRoster().getContact(packet.getFrom()); + if (type == null) { + String presence; if (!from.isBareJid()) { - if (type == null) { - account.updatePresence(from.getResourcepart(), - Presences.parseShow(packet.findChild("show"))); - } else if (type.equals("unavailable")) { - account.removePresence(from.getResourcepart()); - account.deactivateGracePeriod(); - } + presence = from.getResourcepart(); + } else { + presence = ""; } - } else { - Contact contact = account.getRoster().getContact(packet.getFrom()); - if (type == null) { - String presence; - if (!from.isBareJid()) { - presence = from.getResourcepart(); - } else { - presence = ""; - } - int sizeBefore = contact.getPresences().size(); - 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())); + int sizeBefore = contact.getPresences().size(); + 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())); } - boolean online = sizeBefore < contact.getPresences().size(); - updateLastseen(packet, account, true); - mXmppConnectionService.onContactStatusChanged - .onContactStatusChanged(contact, online); - } else if (type.equals("unavailable")) { - if (from.isBareJid()) { - contact.clearPresences(); - } else { - contact.removePresence(from.getResourcepart()); - } - mXmppConnectionService.onContactStatusChanged - .onContactStatusChanged(contact, false); - } else if (type.equals("subscribe")) { - if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { - mXmppConnectionService.sendPresencePacket(account, - mPresenceGenerator.sendPresenceUpdatesTo(contact)); - } else { - contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); - } } - Element nick = packet.findChild("nick", - "http://jabber.org/protocol/nick"); - if (nick != null) { - contact.setPresenceName(nick.getContent()); + boolean online = sizeBefore < contact.getPresences().size(); + updateLastseen(packet, account, false); + mXmppConnectionService.onContactStatusChanged.onContactStatusChanged(contact, online); + } else if (type.equals("unavailable")) { + if (from.isBareJid()) { + contact.clearPresences(); + } else { + contact.removePresence(from.getResourcepart()); + } + mXmppConnectionService.onContactStatusChanged + .onContactStatusChanged(contact, false); + } else if (type.equals("subscribe")) { + if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) { + mXmppConnectionService.sendPresencePacket(account, + mPresenceGenerator.sendPresenceUpdatesTo(contact)); + } else { + contact.setOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST); } } + Element nick = packet.findChild("nick", + "http://jabber.org/protocol/nick"); + if (nick != null) { + contact.setPresenceName(nick.getContent()); + } mXmppConnectionService.updateRosterUi(); } diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 55fcff2e..5fa61491 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -22,7 +22,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 11; + private static final int DATABASE_VERSION = 13; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -65,6 +65,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + Message.BODY + " TEXT, " + Message.ENCRYPTION + " NUMBER, " + Message.STATUS + " NUMBER," + Message.TYPE + " NUMBER, " + Message.RELATIVE_FILE_PATH + " TEXT, " + + Message.SERVER_MSG_ID + " TEXT, " + Message.REMOTE_MSG_ID + " TEXT, FOREIGN KEY(" + Message.CONVERSATION + ") REFERENCES " + Conversation.TABLENAME + "(" + Conversation.UUID @@ -121,6 +122,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.execSQL("delete from "+Contact.TABLENAME); db.execSQL("update "+Account.TABLENAME+" set "+Account.ROSTERVERSION+" = NULL"); } + if (oldVersion < 12 && newVersion >= 12) { + db.execSQL("ALTER TABLE " + Message.TABLENAME + " ADD COLUMN " + + Message.SERVER_MSG_ID + " TEXT"); + } + if (oldVersion < 13 && newVersion >= 13) { + db.execSQL("delete from "+Contact.TABLENAME); + db.execSQL("update "+Account.TABLENAME+" set "+Account.ROSTERVERSION+" = NULL"); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -223,9 +232,9 @@ public class DatabaseBackend extends SQLiteOpenHelper { return conversation; } - public void updateConversation(Conversation conversation) { - SQLiteDatabase db = this.getWritableDatabase(); - String[] args = { conversation.getUuid() }; + public void updateConversation(final Conversation conversation) { + final SQLiteDatabase db = this.getWritableDatabase(); + final String[] args = { conversation.getUuid() }; db.update(Conversation.TABLENAME, conversation.getContentValues(), Conversation.UUID + "=?", args); } diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java new file mode 100644 index 00000000..82111243 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -0,0 +1,366 @@ +package eu.siacs.conversations.services; + +import android.util.Log; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.generator.AbstractGenerator; +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; +import eu.siacs.conversations.xmpp.jid.Jid; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { + + private final XmppConnectionService mXmppConnectionService; + + private final HashSet<Query> queries = new HashSet<Query>(); + private final ArrayList<Query> pendingQueries = new ArrayList<Query>(); + + public enum PagingOrder { + NORMAL, + REVERSE + }; + + public MessageArchiveService(final XmppConnectionService service) { + this.mXmppConnectionService = service; + } + + public void catchup(final Account account) { + long startCatchup = getLastMessageTransmitted(account); + long endCatchup = account.getXmppConnection().getLastSessionEstablished(); + if (startCatchup == 0) { + return; + } else if (endCatchup - startCatchup >= Config.MAM_MAX_CATCHUP) { + startCatchup = endCatchup - Config.MAM_MAX_CATCHUP; + List<Conversation> conversations = mXmppConnectionService.getConversations(); + for (Conversation conversation : conversations) { + if (conversation.getMode() == Conversation.MODE_SINGLE && conversation.getAccount() == account && startCatchup > conversation.getLastMessageTransmitted()) { + this.query(conversation,startCatchup); + } + } + } + final Query query = new Query(account, startCatchup, endCatchup); + this.queries.add(query); + this.execute(query); + } + + private long getLastMessageTransmitted(final Account account) { + long timestamp = 0; + for(final Conversation conversation : mXmppConnectionService.getConversations()) { + if (conversation.getAccount() == account) { + long tmp = conversation.getLastMessageTransmitted(); + if (tmp > timestamp) { + timestamp = tmp; + } + } + } + return timestamp; + } + + public Query query(final Conversation conversation) { + return query(conversation,conversation.getAccount().getXmppConnection().getLastSessionEstablished()); + } + + public Query query(final Conversation conversation, long end) { + return this.query(conversation,conversation.getLastMessageTransmitted(),end); + } + + public Query query(Conversation conversation, long start, long end) { + synchronized (this.queries) { + if (start > end) { + return null; + } + final Query query = new Query(conversation, start, end,PagingOrder.REVERSE); + this.queries.add(query); + this.execute(query); + return query; + } + } + + public void executePendingQueries(final Account account) { + List<Query> pending = new ArrayList<>(); + synchronized(this.pendingQueries) { + for(Iterator<Query> iterator = this.pendingQueries.iterator(); iterator.hasNext();) { + Query query = iterator.next(); + if (query.getAccount() == account) { + pending.add(query); + iterator.remove(); + } + } + } + for(Query query : pending) { + this.execute(query); + } + } + + private void execute(final Query query) { + final Account account= query.getAccount(); + if (account.getStatus() == Account.State.ONLINE) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": running mam query " + query.toString()); + IqPacket packet = this.mXmppConnectionService.getIqGenerator().queryMessageArchiveManagement(query); + this.mXmppConnectionService.sendIqPacket(account, packet, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_ERROR) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString()); + finalizeQuery(query); + } + } + }); + } else { + synchronized (this.pendingQueries) { + this.pendingQueries.add(query); + } + } + } + + private void finalizeQuery(Query query) { + synchronized (this.queries) { + this.queries.remove(query); + } + final Conversation conversation = query.getConversation(); + if (conversation != null) { + conversation.sort(); + if (conversation.setLastMessageTransmitted(query.getEnd())) { + this.mXmppConnectionService.databaseBackend.updateConversation(conversation); + } + conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); + if (query.hasCallback()) { + query.callback(); + } else { + this.mXmppConnectionService.updateConversationUi(); + } + } else { + for(Conversation tmp : this.mXmppConnectionService.getConversations()) { + if (tmp.getAccount() == query.getAccount()) { + tmp.sort(); + if (tmp.setLastMessageTransmitted(query.getEnd())) { + this.mXmppConnectionService.databaseBackend.updateConversation(tmp); + } + } + } + } + } + + public boolean queryInProgress(Conversation conversation, XmppConnectionService.OnMoreMessagesLoaded callback) { + synchronized (this.queries) { + for(Query query : queries) { + if (query.conversation == conversation) { + if (!query.hasCallback() && callback != null) { + query.setCallback(callback); + } + return true; + } + } + return false; + } + } + + public void processFin(Element fin) { + if (fin == null) { + return; + } + Query query = findQuery(fin.getAttribute("queryid")); + if (query == null) { + return; + } + boolean complete = fin.getAttributeAsBoolean("complete"); + Element set = fin.findChild("set","http://jabber.org/protocol/rsm"); + Element last = set == null ? null : set.findChild("last"); + Element first = set == null ? null : set.findChild("first"); + Element relevant = query.getPagingOrder() == PagingOrder.NORMAL ? last : first; + boolean abort = (query.getStart() == 0 && query.getTotalCount() >= Config.PAGE_SIZE) || query.getTotalCount() >= Config.MAM_MAX_MESSAGES; + if (complete || relevant == null || abort) { + this.finalizeQuery(query); + Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages"); + } else { + final Query nextQuery; + if (query.getPagingOrder() == PagingOrder.NORMAL) { + nextQuery = query.next(last == null ? null : last.getContent()); + } else { + nextQuery = query.prev(first == null ? null : first.getContent()); + } + this.execute(nextQuery); + this.finalizeQuery(query); + synchronized (this.queries) { + this.queries.remove(query); + this.queries.add(nextQuery); + } + } + } + + public Query findQuery(String id) { + if (id == null) { + return null; + } + synchronized (this.queries) { + for(Query query : this.queries) { + if (query.getQueryId().equals(id)) { + return query; + } + } + return null; + } + } + + @Override + public void onAdvancedStreamFeaturesAvailable(Account account) { + if (account.getXmppConnection() != null && account.getXmppConnection().getFeatures().mam()) { + this.catchup(account); + } + } + + public class Query { + private int totalCount = 0; + private int messageCount = 0; + private long start; + private long end; + private Jid with = null; + private String queryId; + private String reference = null; + private Account account; + private Conversation conversation; + private PagingOrder pagingOrder = PagingOrder.NORMAL; + private XmppConnectionService.OnMoreMessagesLoaded callback = null; + + + public Query(Conversation conversation, long start, long end) { + this(conversation.getAccount(), start, end); + this.conversation = conversation; + this.with = conversation.getJid().toBareJid(); + } + + public Query(Conversation conversation, long start, long end, PagingOrder order) { + this(conversation,start,end); + this.pagingOrder = order; + } + + public Query(Account account, long start, long end) { + this.account = account; + this.start = start; + this.end = end; + this.queryId = new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); + } + + private Query page(String reference) { + Query query = new Query(this.account,this.start,this.end); + query.reference = reference; + query.conversation = conversation; + query.with = with; + query.totalCount = totalCount; + query.callback = callback; + return query; + } + + public Query next(String reference) { + Query query = page(reference); + query.pagingOrder = PagingOrder.NORMAL; + return query; + } + + public Query prev(String reference) { + Query query = page(reference); + query.pagingOrder = PagingOrder.REVERSE; + return query; + } + + public String getReference() { + return reference; + } + + public PagingOrder getPagingOrder() { + return this.pagingOrder; + } + + public String getQueryId() { + return queryId; + } + + public Jid getWith() { + return with; + } + + public long getStart() { + return start; + } + + public void setCallback(XmppConnectionService.OnMoreMessagesLoaded callback) { + this.callback = callback; + } + + public void callback() { + if (this.callback != null) { + this.callback.onMoreMessagesLoaded(messageCount,conversation); + if (messageCount == 0) { + this.callback.informUser(R.string.no_more_history_on_server); + } + } + } + + public long getEnd() { + return end; + } + + public Conversation getConversation() { + return conversation; + } + + public Account getAccount() { + return this.account; + } + + public void incrementTotalCount() { + this.totalCount++; + } + + public void incrementMessageCount() { + this.messageCount++; + } + + public int getTotalCount() { + return this.totalCount; + } + + public int getMessageCount() { + return this.messageCount; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("with="); + if (this.with==null) { + builder.append("*"); + } else { + builder.append(with.toString()); + } + builder.append(", start="); + builder.append(AbstractGenerator.getTimestamp(this.start)); + builder.append(", end="); + builder.append(AbstractGenerator.getTimestamp(this.end)); + if (this.reference!=null) { + if (this.pagingOrder == PagingOrder.NORMAL) { + builder.append(", after="); + } else { + builder.append(", before="); + } + builder.append(this.reference); + } + return builder.toString(); + } + + public boolean hasCallback() { + return this.callback != null; + } + } +} diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index f649f9d4..a30cf2f1 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -16,9 +16,11 @@ import android.support.v4.app.NotificationCompat.Builder; import android.support.v4.app.TaskStackBuilder; import android.text.Html; import android.util.DisplayMetrics; +import android.util.Log; import java.io.FileNotFoundException; import java.util.ArrayList; +import java.util.Calendar; import java.util.LinkedHashMap; import java.util.List; import java.util.regex.Matcher; @@ -33,16 +35,17 @@ import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.ManageAccountActivity; +import eu.siacs.conversations.ui.TimePreference; public class NotificationService { private XmppConnectionService mXmppConnectionService; - private LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<String, ArrayList<Message>>(); + private final LinkedHashMap<String, ArrayList<Message>> notifications = new LinkedHashMap<>(); - public static int NOTIFICATION_ID = 0x2342; - public static int FOREGROUND_NOTIFICATION_ID = 0x8899; - public static int ERROR_NOTIFICATION_ID = 0x5678; + public static final int NOTIFICATION_ID = 0x2342; + public static final int FOREGROUND_NOTIFICATION_ID = 0x8899; + public static final int ERROR_NOTIFICATION_ID = 0x5678; private Conversation mOpenConversation; private boolean mIsInForeground; @@ -52,46 +55,61 @@ public class NotificationService { this.mXmppConnectionService = service; } - public boolean notify(Message message) { + public boolean notify(final Message message) { return (message.getStatus() == Message.STATUS_RECEIVED) - && notificationsEnabled() - && !message.getConversation().isMuted() - && (message.getConversation().getMode() == Conversation.MODE_SINGLE + && notificationsEnabled() + && !message.getConversation().isMuted() + && (message.getConversation().getMode() == Conversation.MODE_SINGLE || conferenceNotificationsEnabled() || wasHighlightedOrPrivate(message) - ); + ); } public boolean notificationsEnabled() { return mXmppConnectionService.getPreferences().getBoolean("show_notification", true); } + public boolean isQuietHours() { + if (!mXmppConnectionService.getPreferences().getBoolean("enable_quiet_hours", false)) { + return false; + } + final long startTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_start", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; + final long endTime = mXmppConnectionService.getPreferences().getLong("quiet_hours_end", TimePreference.DEFAULT_VALUE) % Config.MILLISECONDS_IN_DAY; + final long nowTime = Calendar.getInstance().getTimeInMillis() % Config.MILLISECONDS_IN_DAY; + + if (endTime < startTime) { + return nowTime > startTime || nowTime < endTime; + } else { + return nowTime > startTime && nowTime < endTime; + } + } + public boolean conferenceNotificationsEnabled() { return mXmppConnectionService.getPreferences().getBoolean("always_notify_in_conference", false); } - public void push(Message message) { + public void push(final Message message) { if (!notify(message)) { return; } - PowerManager pm = (PowerManager) mXmppConnectionService - .getSystemService(Context.POWER_SERVICE); - boolean isScreenOn = pm.isScreenOn(); + final PowerManager pm = (PowerManager) mXmppConnectionService + .getSystemService(Context.POWER_SERVICE); + final boolean isScreenOn = pm.isScreenOn(); if (this.mIsInForeground && isScreenOn && this.mOpenConversation == message.getConversation()) { return; - } + } synchronized (notifications) { - String conversationUuid = message.getConversationUuid(); + final String conversationUuid = message.getConversationUuid(); if (notifications.containsKey(conversationUuid)) { notifications.get(conversationUuid).add(message); } else { - ArrayList<Message> mList = new ArrayList<Message>(); + final ArrayList<Message> mList = new ArrayList<>(); mList.add(message); notifications.put(conversationUuid, mList); } - Account account = message.getConversation().getAccount(); + final Account account = message.getConversation().getAccount(); updateNotification((!(this.mIsInForeground && this.mOpenConversation == null) || !isScreenOn) && !account.inGracePeriod() && !this.inMiniGracePeriod(account)); @@ -106,21 +124,20 @@ public class NotificationService { } } - public void clear(Conversation conversation) { + public void clear(final Conversation conversation) { synchronized (notifications) { notifications.remove(conversation.getUuid()); updateNotification(false); } } - private void updateNotification(boolean notify) { - NotificationManager notificationManager = (NotificationManager) mXmppConnectionService - .getSystemService(Context.NOTIFICATION_SERVICE); - SharedPreferences preferences = mXmppConnectionService.getPreferences(); + private void updateNotification(final boolean notify) { + final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService + .getSystemService(Context.NOTIFICATION_SERVICE); + final SharedPreferences preferences = mXmppConnectionService.getPreferences(); - String ringtone = preferences.getString("notification_ringtone", null); - boolean vibrate = preferences.getBoolean("vibrate_on_notification", - true); + final String ringtone = preferences.getString("notification_ringtone", null); + final boolean vibrate = preferences.getBoolean("vibrate_on_notification", true); if (notifications.size() == 0) { notificationManager.cancel(NOTIFICATION_ID); @@ -128,16 +145,16 @@ public class NotificationService { if (notify) { this.markLastNotification(); } - Builder mBuilder; + final Builder mBuilder; if (notifications.size() == 1) { mBuilder = buildSingleConversations(notify); } else { mBuilder = buildMultipleConversation(); } - if (notify) { + if (notify && !isQuietHours()) { if (vibrate) { - int dat = 70; - long[] pattern = {0, 3 * dat, dat, dat}; + final int dat = 70; + final long[] pattern = {0, 3 * dat, dat, dat}; mBuilder.setVibrate(pattern); } if (ringtone != null) { @@ -147,27 +164,27 @@ public class NotificationService { mBuilder.setSmallIcon(R.drawable.ic_notification); mBuilder.setDeleteIntent(createDeleteIntent()); mBuilder.setLights(0xffffffff, 2000, 4000); - Notification notification = mBuilder.build(); + final Notification notification = mBuilder.build(); notificationManager.notify(NOTIFICATION_ID, notification); } } private Builder buildMultipleConversation() { - Builder mBuilder = new NotificationCompat.Builder( + final Builder mBuilder = new NotificationCompat.Builder( mXmppConnectionService); NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(); style.setBigContentTitle(notifications.size() + " " + mXmppConnectionService .getString(R.string.unread_conversations)); - StringBuilder names = new StringBuilder(); + final StringBuilder names = new StringBuilder(); Conversation conversation = null; for (ArrayList<Message> messages : notifications.values()) { if (messages.size() > 0) { conversation = messages.get(0).getConversation(); String name = conversation.getName(); style.addLine(Html.fromHtml("<b>" + name + "</b> " - + getReadableBody(messages.get(0)))); + + getReadableBody(messages.get(0)))); names.append(name); names.append(", "); } @@ -183,46 +200,45 @@ public class NotificationService { mBuilder.setStyle(style); if (conversation != null) { mBuilder.setContentIntent(createContentIntent(conversation - .getUuid())); + .getUuid())); } return mBuilder; } - private Builder buildSingleConversations(boolean notify) { - Builder mBuilder = new NotificationCompat.Builder( + private Builder buildSingleConversations(final boolean notify) { + final Builder mBuilder = new NotificationCompat.Builder( mXmppConnectionService); - ArrayList<Message> messages = notifications.values().iterator().next(); + final ArrayList<Message> messages = notifications.values().iterator().next(); if (messages.size() >= 1) { - Conversation conversation = messages.get(0).getConversation(); + final Conversation conversation = messages.get(0).getConversation(); mBuilder.setLargeIcon(mXmppConnectionService.getAvatarService() .get(conversation, getPixel(64))); mBuilder.setContentTitle(conversation.getName()); - Message message; + final Message message; if ((message = getImage(messages)) != null) { modifyForImage(mBuilder, message, messages, notify); } else { modifyForTextOnly(mBuilder, messages, notify); } mBuilder.setContentIntent(createContentIntent(conversation - .getUuid())); + .getUuid())); } return mBuilder; - } - private void modifyForImage(Builder builder, Message message, - ArrayList<Message> messages, boolean notify) { + private void modifyForImage(final Builder builder, final Message message, + final ArrayList<Message> messages, final boolean notify) { try { - Bitmap bitmap = mXmppConnectionService.getFileBackend() - .getThumbnail(message, getPixel(288), false); - ArrayList<Message> tmp = new ArrayList<Message>(); - for (Message msg : messages) { + final Bitmap bitmap = mXmppConnectionService.getFileBackend() + .getThumbnail(message, getPixel(288), false); + final ArrayList<Message> tmp = new ArrayList<>(); + for (final Message msg : messages) { if (msg.getType() == Message.TYPE_TEXT && msg.getDownloadable() == null) { tmp.add(msg); - } + } } - BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); + final BigPictureStyle bigPictureStyle = new NotificationCompat.BigPictureStyle(); bigPictureStyle.bigPicture(bitmap); if (tmp.size() > 0) { bigPictureStyle.setSummaryText(getMergedBodies(tmp)); @@ -231,13 +247,13 @@ public class NotificationService { builder.setContentText(mXmppConnectionService.getString(R.string.image_file)); } builder.setStyle(bigPictureStyle); - } catch (FileNotFoundException e) { + } catch (final FileNotFoundException e) { modifyForTextOnly(builder, messages, notify); } } - private void modifyForTextOnly(Builder builder, - ArrayList<Message> messages, boolean notify) { + private void modifyForTextOnly(final Builder builder, + final ArrayList<Message> messages, final boolean notify) { builder.setStyle(new NotificationCompat.BigTextStyle() .bigText(getMergedBodies(messages))); builder.setContentText(getReadableBody(messages.get(0))); @@ -246,19 +262,19 @@ public class NotificationService { } } - private Message getImage(ArrayList<Message> messages) { - for (Message message : messages) { + private Message getImage(final ArrayList<Message> messages) { + for (final Message message : messages) { if (message.getType() == Message.TYPE_IMAGE && message.getDownloadable() == null && message.getEncryption() != Message.ENCRYPTION_PGP) { return message; - } + } } return null; } - private String getMergedBodies(ArrayList<Message> messages) { - StringBuilder text = new StringBuilder(); + private String getMergedBodies(final ArrayList<Message> messages) { + final StringBuilder text = new StringBuilder(); for (int i = 0; i < messages.size(); ++i) { text.append(getReadableBody(messages.get(i))); if (i != messages.size() - 1) { @@ -268,10 +284,10 @@ public class NotificationService { return text.toString(); } - private String getReadableBody(Message message) { + private String getReadableBody(final Message message) { if (message.getDownloadable() != null && (message.getDownloadable().getStatus() == Downloadable.STATUS_OFFER || message - .getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) { + .getDownloadable().getStatus() == Downloadable.STATUS_OFFER_CHECK_FILESIZE)) { if (message.getType() == Message.TYPE_FILE) { return mXmppConnectionService.getString(R.string.file_offered_for_download); } else { @@ -283,27 +299,27 @@ public class NotificationService { R.string.encrypted_message_received).toString(); } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTION_FAILED) { return mXmppConnectionService.getText(R.string.decryption_failed) - .toString(); + .toString(); } else if (message.getType() == Message.TYPE_FILE) { DownloadableFile file = mXmppConnectionService.getFileBackend().getFile(message); return mXmppConnectionService.getString(R.string.file,file.getMimeType()); } else if (message.getType() == Message.TYPE_IMAGE) { return mXmppConnectionService.getText(R.string.image_file) - .toString(); + .toString(); } else { return message.getBody().trim(); } } - private PendingIntent createContentIntent(String conversationUuid) { - TaskStackBuilder stackBuilder = TaskStackBuilder - .create(mXmppConnectionService); + private PendingIntent createContentIntent(final String conversationUuid) { + final TaskStackBuilder stackBuilder = TaskStackBuilder + .create(mXmppConnectionService); stackBuilder.addParentStack(ConversationActivity.class); - Intent viewConversationIntent = new Intent(mXmppConnectionService, + final Intent viewConversationIntent = new Intent(mXmppConnectionService, ConversationActivity.class); viewConversationIntent.setAction(Intent.ACTION_VIEW); - if (conversationUuid!=null) { + if (conversationUuid != null) { viewConversationIntent.putExtra(ConversationActivity.CONVERSATION, conversationUuid); viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION); @@ -311,36 +327,34 @@ public class NotificationService { stackBuilder.addNextIntent(viewConversationIntent); - PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, - PendingIntent.FLAG_UPDATE_CURRENT); - return resultPendingIntent; + return stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT); } private PendingIntent createDeleteIntent() { - Intent intent = new Intent(mXmppConnectionService, + final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_CLEAR_NOTIFICATION); return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); } private PendingIntent createDisableForeground() { - Intent intent = new Intent(mXmppConnectionService, + final Intent intent = new Intent(mXmppConnectionService, XmppConnectionService.class); intent.setAction(XmppConnectionService.ACTION_DISABLE_FOREGROUND); return PendingIntent.getService(mXmppConnectionService, 0, intent, 0); } - private boolean wasHighlightedOrPrivate(Message message) { - String nick = message.getConversation().getMucOptions().getActualNick(); - Pattern highlight = generateNickHighlightPattern(nick); + private boolean wasHighlightedOrPrivate(final Message message) { + final String nick = message.getConversation().getMucOptions().getActualNick(); + final Pattern highlight = generateNickHighlightPattern(nick); if (message.getBody() == null || nick == null) { return false; } - Matcher m = highlight.matcher(message.getBody()); + final Matcher m = highlight.matcher(message.getBody()); return (m.find() || message.getType() == Message.TYPE_PRIVATE); } - private static Pattern generateNickHighlightPattern(String nick) { + private static Pattern generateNickHighlightPattern(final String nick) { // We expect a word boundary, i.e. space or start of string, followed by // the // nick (matched in case-insensitive manner), followed by optional @@ -350,17 +364,20 @@ public class NotificationService { Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); } - public void setOpenConversation(Conversation conversation) { + public void setOpenConversation(final Conversation conversation) { this.mOpenConversation = conversation; } - public void setIsInForeground(boolean foreground) { + public void setIsInForeground(final boolean foreground) { + if (foreground != this.mIsInForeground) { + Log.d(Config.LOGTAG,"setIsInForeground("+Boolean.toString(foreground)+")"); + } this.mIsInForeground = foreground; } - private int getPixel(int dp) { - DisplayMetrics metrics = mXmppConnectionService.getResources() - .getDisplayMetrics(); + private int getPixel(final int dp) { + final DisplayMetrics metrics = mXmppConnectionService.getResources() + .getDisplayMetrics(); return ((int) (dp * metrics.density)); } @@ -368,14 +385,14 @@ public class NotificationService { this.mLastNotification = SystemClock.elapsedRealtime(); } - private boolean inMiniGracePeriod(Account account) { - int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD - : Config.MINI_GRACE_PERIOD * 2; + private boolean inMiniGracePeriod(final Account account) { + final int miniGrace = account.getStatus() == Account.State.ONLINE ? Config.MINI_GRACE_PERIOD + : Config.MINI_GRACE_PERIOD * 2; return SystemClock.elapsedRealtime() < (this.mLastNotification + miniGrace); } public Notification createForegroundNotification() { - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); + final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); mBuilder.setSmallIcon(R.drawable.ic_stat_communication_import_export); mBuilder.setContentTitle(mXmppConnectionService.getString(R.string.conversations_foreground_service)); mBuilder.setContentText(mXmppConnectionService.getString(R.string.touch_to_disable)); @@ -386,14 +403,14 @@ public class NotificationService { } public void updateErrorNotification() { - NotificationManager mNotificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); - List<Account> errors = new ArrayList<>(); - for (Account account : mXmppConnectionService.getAccounts()) { + final NotificationManager mNotificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); + final List<Account> errors = new ArrayList<>(); + for (final Account account : mXmppConnectionService.getAccounts()) { if (account.hasErrorStatus()) { errors.add(account); } } - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); + final NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(mXmppConnectionService); if (errors.size() == 0) { mNotificationManager.cancel(ERROR_NOTIFICATION_ID); return; @@ -410,13 +427,12 @@ public class NotificationService { TaskStackBuilder stackBuilder = TaskStackBuilder.create(mXmppConnectionService); stackBuilder.addParentStack(ConversationActivity.class); - Intent manageAccountsIntent = new Intent(mXmppConnectionService,ManageAccountActivity.class); + final Intent manageAccountsIntent = new Intent(mXmppConnectionService,ManageAccountActivity.class); stackBuilder.addNextIntent(manageAccountsIntent); - PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT); + final PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,PendingIntent.FLAG_UPDATE_CURRENT); mBuilder.setContentIntent(resultPendingIntent); - Notification notification = mBuilder.build(); - mNotificationManager.notify(ERROR_NOTIFICATION_ID, notification); + mNotificationManager.notify(ERROR_NOTIFICATION_ID, mBuilder.build()); } } diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index 41a40224..6bdc55a1 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -32,20 +32,16 @@ import net.java.otr4j.session.SessionStatus; import org.openintents.openpgp.util.OpenPgpApi; import org.openintents.openpgp.util.OpenPgpServiceConnection; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.OutputStream; import java.math.BigInteger; import java.security.SecureRandom; -import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.Hashtable; import java.util.List; import java.util.Locale; -import java.util.TimeZone; +import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import de.duenndns.ssl.MemorizingTrustManager; @@ -53,11 +49,11 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Downloadable; -import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.DownloadablePlaceholder; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; @@ -78,12 +74,17 @@ 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.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnBindListener; import eu.siacs.conversations.xmpp.OnContactStatusChanged; import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnMessageAcknowledged; +import eu.siacs.conversations.xmpp.OnMessagePacketReceived; +import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.OnStatusChanged; +import eu.siacs.conversations.xmpp.OnUpdateBlocklist; +import eu.siacs.conversations.xmpp.PacketReceived; import eu.siacs.conversations.xmpp.XmppConnection; import eu.siacs.conversations.xmpp.forms.Data; import eu.siacs.conversations.xmpp.forms.Field; @@ -97,11 +98,12 @@ import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.MessagePacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; -public class XmppConnectionService extends Service { +public class XmppConnectionService extends Service implements OnPhoneContactsLoadedListener { + + public static final String ACTION_CLEAR_NOTIFICATION = "clear_notification"; + private static final String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; + public static final String ACTION_DISABLE_FOREGROUND = "disable_foreground"; - public static String ACTION_CLEAR_NOTIFICATION = "clear_notification"; - private static String ACTION_MERGE_PHONE_CONTACTS = "merge_phone_contacts"; - public static String ACTION_DISABLE_FOREGROUND = "disable_foreground"; private ContentObserver contactObserver = new ContentObserver(null) { @Override public void onChange(boolean selfChange) { @@ -126,7 +128,7 @@ public class XmppConnectionService extends Service { conversation.resetOtrSession(); } if (online && (contact.getPresences().size() == 1)) { - sendUnsendMessages(conversation); + sendUnsentMessages(conversation); } } } @@ -135,18 +137,19 @@ public class XmppConnectionService extends Service { private MemorizingTrustManager mMemorizingTrustManager; private NotificationService mNotificationService = new NotificationService( this); - private MessageParser mMessageParser = new MessageParser(this); - private PresenceParser mPresenceParser = new PresenceParser(this); + private OnMessagePacketReceived mMessageParser = new MessageParser(this); + private OnPresencePacketReceived mPresenceParser = new PresenceParser(this); private IqParser mIqParser = new IqParser(this); private MessageGenerator mMessageGenerator = new MessageGenerator(this); private PresenceGenerator mPresenceGenerator = new PresenceGenerator(this); private List<Account> accounts; - private final CopyOnWriteArrayList<Conversation> conversations = new CopyOnWriteArrayList<Conversation>(); + private final List<Conversation> conversations = new CopyOnWriteArrayList<>(); private JingleConnectionManager mJingleConnectionManager = new JingleConnectionManager( this); private HttpConnectionManager mHttpConnectionManager = new HttpConnectionManager( this); private AvatarService mAvatarService = new AvatarService(this); + private MessageArchiveService mMessageArchiveService = new MessageArchiveService(this); private OnConversationUpdate mOnConversationUpdate = null; private Integer convChangedListenerCount = 0; private OnAccountUpdate mOnAccountUpdate = null; @@ -165,12 +168,13 @@ public class XmppConnectionService extends Service { for (Conversation conversation : account.pendingConferenceJoins) { joinMuc(conversation); } + mMessageArchiveService.executePendingQueries(account); mJingleConnectionManager.cancelInTransmission(); List<Conversation> conversations = getConversations(); for (Conversation conversation : conversations) { if (conversation.getAccount() == account) { conversation.startOtrIfNeeded(); - sendUnsendMessages(conversation); + sendUnsentMessages(conversation); } } if (connection != null && connection.getFeatures().csi()) { @@ -209,13 +213,16 @@ public class XmppConnectionService extends Service { getNotificationService().updateErrorNotification(); } }; + private int accountChangedListenerCount = 0; private OnRosterUpdate mOnRosterUpdate = null; + private OnUpdateBlocklist mOnUpdateBlocklist = null; + private int updateBlocklistListenerCount = 0; private int rosterChangedListenerCount = 0; private OnMucRosterUpdate mOnMucRosterUpdate = null; private int mucRosterChangedListenerCount = 0; private SecureRandom mRandom; - private FileObserver fileObserver = new FileObserver( + private final FileObserver fileObserver = new FileObserver( FileBackend.getConversationsImageDirectory()) { @Override @@ -225,7 +232,7 @@ public class XmppConnectionService extends Service { } } }; - private OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { + private final OnJinglePacketReceived jingleListener = new OnJinglePacketReceived() { @Override public void onJinglePacketReceived(Account account, JinglePacket packet) { @@ -239,43 +246,41 @@ public class XmppConnectionService extends Service { private PendingIntent pendingPingIntent = null; private WakeLock wakeLock; private PowerManager pm; - private OnBindListener mOnBindListener = new OnBindListener() { + private final OnBindListener mOnBindListener = new OnBindListener() { @Override public void onBind(final Account account) { account.getRoster().clearPresences(); - account.clearPresences(); // self presences account.pendingConferenceJoins.clear(); account.pendingConferenceLeaves.clear(); fetchRosterFromServer(account); fetchBookmarks(account); - sendPresencePacket(account, - mPresenceGenerator.sendPresence(account)); + sendPresencePacket(account,mPresenceGenerator.sendPresence(account)); connectMultiModeConversations(account); updateConversationUi(); } }; - private OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { + private final OnMessageAcknowledged mOnMessageAcknowledgedListener = new OnMessageAcknowledged() { @Override public void onMessageAcknowledged(Account account, String uuid) { - for (Conversation conversation : getConversations()) { + for (final Conversation conversation : getConversations()) { if (conversation.getAccount() == account) { - for (Message message : conversation.getMessages()) { - if ((message.getStatus() == Message.STATUS_UNSEND || message - .getStatus() == Message.STATUS_WAITING) - && message.getUuid().equals(uuid)) { - markMessage(message, Message.STATUS_SEND); - return; - } + Message message = conversation.findUnsentMessageWithUuid(uuid); + if (message != null) { + markMessage(message, Message.STATUS_SEND); + if (conversation.setLastMessageTransmitted(System.currentTimeMillis())) { + databaseBackend.updateConversation(conversation); + } } } } } }; private LruCache<String, Bitmap> mBitmapCache; - private IqGenerator mIqGenerator = new IqGenerator(this); + private final IqGenerator mIqGenerator = new IqGenerator(this); + private Thread mPhoneContactMergerThread; public PgpEngine getPgpEngine() { if (pgpServiceConnection.isBound()) { @@ -299,7 +304,9 @@ public class XmppConnectionService extends Service { return this.mAvatarService; } - public void attachFileToConversation(Conversation conversation, final Uri uri, final UiCallback<Message> callback) { + public void attachFileToConversation(final Conversation conversation, + final Uri uri, + final UiCallback<Message> callback) { final Message message; if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { message = new Message(conversation, "", @@ -359,13 +366,13 @@ public class XmppConnectionService extends Service { @Override public void run() { try { - DownloadableFile file = getFileBackend().copyImageToPrivateStorage(message, uri); + getFileBackend().copyImageToPrivateStorage(message, uri); if (conversation.getNextEncryption(forceEncryption()) == Message.ENCRYPTION_PGP) { getPgpEngine().encrypt(message, callback); } else { callback.success(message); } - } catch (FileBackend.FileCopyException e) { + } catch (final FileBackend.FileCopyException e) { callback.error(e.getResId(), message); } } @@ -384,7 +391,7 @@ public class XmppConnectionService extends Service { public int onStartCommand(Intent intent, int flags, int startId) { if (intent != null && intent.getAction() != null) { if (intent.getAction().equals(ACTION_MERGE_PHONE_CONTACTS)) { - mergePhoneContactsWithRoster(); + PhoneHelper.loadPhoneContacts(getApplicationContext(), new ArrayList<Bundle>(), this); return START_STICKY; } else if (intent.getAction().equals(Intent.ACTION_SHUTDOWN)) { logoutAndSave(); @@ -479,11 +486,11 @@ public class XmppConnectionService extends Service { this.mMemorizingTrustManager = new MemorizingTrustManager( getApplicationContext()); - int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); - int cacheSize = maxMemory / 8; + final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); + final int cacheSize = maxMemory / 8; this.mBitmapCache = new LruCache<String, Bitmap>(cacheSize) { @Override - protected int sizeOf(String key, Bitmap bitmap) { + protected int sizeOf(final String key, final Bitmap bitmap) { return bitmap.getByteCount() / 1024; } }; @@ -491,12 +498,12 @@ public class XmppConnectionService extends Service { this.databaseBackend = DatabaseBackend.getInstance(getApplicationContext()); this.accounts = databaseBackend.getAccounts(); - for (Account account : this.accounts) { + for (final Account account : this.accounts) { account.initOtrEngine(this); this.databaseBackend.readRoster(account.getRoster()); } initConversations(); - this.mergePhoneContactsWithRoster(); + PhoneHelper.loadPhoneContacts(getApplicationContext(),new ArrayList<Bundle>(), this); getContentResolver().registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, contactObserver); this.fileObserver.startWatching(); @@ -517,7 +524,7 @@ public class XmppConnectionService extends Service { } @Override - public void onTaskRemoved(Intent rootIntent) { + public void onTaskRemoved(final Intent rootIntent) { super.onTaskRemoved(rootIntent); if (!getPreferences().getBoolean("keep_foreground_service",false)) { this.logoutAndSave(); @@ -525,7 +532,7 @@ public class XmppConnectionService extends Service { } private void logoutAndSave() { - for (Account account : accounts) { + for (final Account account : accounts) { databaseBackend.writeRoster(account.getRoster()); if (account.getXmppConnection() != null) { disconnect(account, false); @@ -578,26 +585,26 @@ public class XmppConnectionService extends Service { } - public XmppConnection createConnection(Account account) { - SharedPreferences sharedPref = getPreferences(); + public XmppConnection createConnection(final Account account) { + final SharedPreferences sharedPref = getPreferences(); account.setResource(sharedPref.getString("resource", "mobile") .toLowerCase(Locale.getDefault())); - XmppConnection connection = new XmppConnection(account, this); + final XmppConnection connection = new XmppConnection(account, this); connection.setOnMessagePacketReceivedListener(this.mMessageParser); connection.setOnStatusChangedListener(this.statusListener); connection.setOnPresencePacketReceivedListener(this.mPresenceParser); connection.setOnUnregisteredIqPacketReceivedListener(this.mIqParser); connection.setOnJinglePacketReceivedListener(this.jingleListener); connection.setOnBindListener(this.mOnBindListener); - connection - .setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); + connection.setOnMessageAcknowledgeListener(this.mOnMessageAcknowledgedListener); + connection.addOnAdvancedStreamFeaturesAvailableListener(this.mMessageArchiveService); return connection; } - public void sendMessage(Message message) { - Account account = message.getConversation().getAccount(); + public void sendMessage(final Message message) { + final Account account = message.getConversation().getAccount(); account.deactivateGracePeriod(); - Conversation conv = message.getConversation(); + final Conversation conv = message.getConversation(); MessagePacket packet = null; boolean saveInDb = true; boolean send = false; @@ -641,12 +648,22 @@ public class XmppConnectionService extends Service { } } else if (message.getEncryption() == Message.ENCRYPTION_DECRYPTED) { message.getConversation().endOtrIfNeeded(); - failWaitingOtrMessages(message.getConversation()); + message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + markMessage(message,Message.STATUS_SEND_FAILED); + } + }); packet = mMessageGenerator.generatePgpChat(message); send = true; } else { message.getConversation().endOtrIfNeeded(); - failWaitingOtrMessages(message.getConversation()); + message.getConversation().findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + @Override + public void onMessageFound(Message message) { + markMessage(message,Message.STATUS_SEND_FAILED); + } + }); packet = mMessageGenerator.generateChat(message); send = true; } @@ -689,16 +706,17 @@ public class XmppConnectionService extends Service { updateConversationUi(); } - private void sendUnsendMessages(Conversation conversation) { - for (int i = 0; i < conversation.getMessages().size(); ++i) { - int status = conversation.getMessages().get(i).getStatus(); - if (status == Message.STATUS_WAITING) { - resendMessage(conversation.getMessages().get(i)); + private void sendUnsentMessages(final Conversation conversation) { + conversation.findWaitingMessages(new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { + resendMessage(message); } - } + }); } - private void resendMessage(Message message) { + private void resendMessage(final Message message) { Account account = message.getConversation().getAccount(); MessagePacket packet = null; if (message.getEncryption() == Message.ENCRYPTION_OTR) { @@ -725,7 +743,7 @@ public class XmppConnectionService extends Service { } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { mJingleConnectionManager.createNewConnection(message); } - } catch (InvalidJidException e) { + } catch (final InvalidJidException ignored) { } } @@ -768,47 +786,35 @@ public class XmppConnectionService extends Service { } } - public void fetchRosterFromServer(Account account) { - IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + public void fetchRosterFromServer(final Account account) { + final IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); if (!"".equals(account.getRosterVersion())) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster version " + account.getRosterVersion()); } else { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": fetching roster"); } - iqPacket.query("jabber:iq:roster").setAttribute("ver", + iqPacket.query(Xmlns.ROSTER).setAttribute("ver", account.getRosterVersion()); - account.getXmppConnection().sendIqPacket(iqPacket, - new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, - IqPacket packet) { - Element query = packet.findChild("query"); - if (query != null) { - account.getRoster().markAllAsNotInRoster(); - mIqParser.rosterItems(account, query); - } - } - }); + account.getXmppConnection().sendIqPacket(iqPacket, mIqParser); } - public void fetchBookmarks(Account account) { - IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); - Element query = iqPacket.query("jabber:iq:private"); + public void fetchBookmarks(final Account account) { + final IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET); + final Element query = iqPacket.query("jabber:iq:private"); query.addChild("storage", "storage:bookmarks"); - OnIqPacketReceived callback = new OnIqPacketReceived() { + final PacketReceived callback = new OnIqPacketReceived() { @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Element query = packet.query(); - List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); - Element storage = query.findChild("storage", + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final Element query = packet.query(); + final List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); + final Element storage = query.findChild("storage", "storage:bookmarks"); if (storage != null) { - for (Element item : storage.getChildren()) { + for (final Element item : storage.getChildren()) { if (item.getName().equals("conference")) { - Bookmark bookmark = Bookmark.parse(item, account); + final Bookmark bookmark = Bookmark.parse(item, account); bookmarks.add(bookmark); Conversation conversation = find(bookmark); if (conversation != null) { @@ -826,7 +832,6 @@ public class XmppConnectionService extends Service { } }; sendIqPacket(account, iqPacket, callback); - } public void pushBookmarks(Account account) { @@ -839,53 +844,56 @@ public class XmppConnectionService extends Service { sendIqPacket(account, iqPacket, null); } - private void mergePhoneContactsWithRoster() { - PhoneHelper.loadPhoneContacts(getApplicationContext(), - new OnPhoneContactsLoadedListener() { - @Override - public void onPhoneContactsLoaded(List<Bundle> phoneContacts) { - for (Account account : accounts) { - account.getRoster().clearSystemAccounts(); + public void onPhoneContactsLoaded(final List<Bundle> phoneContacts) { + if (mPhoneContactMergerThread != null) { + mPhoneContactMergerThread.interrupt(); + } + mPhoneContactMergerThread = new Thread(new Runnable() { + @Override + public void run() { + Log.d(Config.LOGTAG,"start merging phone contacts with roster"); + for (Account account : accounts) { + account.getRoster().clearSystemAccounts(); + for (Bundle phoneContact : phoneContacts) { + if (Thread.interrupted()) { + Log.d(Config.LOGTAG,"interrupted merging phone contacts"); + return; } - for (Bundle phoneContact : phoneContacts) { - for (Account account : accounts) { - Jid jid; - try { - jid = Jid.fromString(phoneContact.getString("jid")); - } catch (final InvalidJidException e) { - // TODO: Warn if contact import fails here? - break; - } - final Contact contact = account.getRoster() - .getContact(jid); - String systemAccount = phoneContact - .getInt("phoneid") - + "#" - + phoneContact.getString("lookup"); - contact.setSystemAccount(systemAccount); - contact.setPhotoUri(phoneContact - .getString("photouri")); - contact.setSystemName(phoneContact - .getString("displayname")); - getAvatarService().clear(contact); - } + Jid jid; + try { + jid = Jid.fromString(phoneContact.getString("jid")); + } catch (final InvalidJidException e) { + continue; } + final Contact contact = account.getRoster().getContact(jid); + String systemAccount = phoneContact.getInt("phoneid") + + "#" + + phoneContact.getString("lookup"); + contact.setSystemAccount(systemAccount); + contact.setPhotoUri(phoneContact.getString("photouri")); + getAvatarService().clear(contact); + contact.setSystemName(phoneContact.getString("displayname")); } - }); + } + Log.d(Config.LOGTAG,"finished merging phone contacts"); + updateAccountUi(); + } + }); + mPhoneContactMergerThread.start(); } private void initConversations() { synchronized (this.conversations) { - Hashtable<String, Account> accountLookupTable = new Hashtable<>(); + final Map<String, Account> accountLookupTable = new Hashtable<>(); for (Account account : this.accounts) { accountLookupTable.put(account.getUuid(), account); } this.conversations.addAll(databaseBackend.getConversations(Conversation.STATUS_AVAILABLE)); - for (Conversation conv : this.conversations) { - Account account = accountLookupTable.get(conv.getAccountUuid()); - conv.setAccount(account); - conv.addAll(0, databaseBackend.getMessages(conv, 50)); - checkDeletedFiles(conv); + for (Conversation conversation : this.conversations) { + Account account = accountLookupTable.get(conversation.getAccountUuid()); + conversation.setAccount(account); + conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); + checkDeletedFiles(conversation); } } } @@ -895,28 +903,26 @@ public class XmppConnectionService extends Service { } private void checkDeletedFiles(Conversation conversation) { - for (Message message : conversation.getMessages()) { - if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) - && message.getEncryption() != Message.ENCRYPTION_PGP) { + conversation.findMessagesWithFiles(new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { if (!getFileBackend().isFileAvailable(message)) { message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); } - } - } + } + }); } private void markFileDeleted(String uuid) { for (Conversation conversation : getConversations()) { - for (Message message : conversation.getMessages()) { - if (message.getType() == Message.TYPE_IMAGE - && message.getEncryption() != Message.ENCRYPTION_PGP - && message.getUuid().equals(uuid)) { - if (!getFileBackend().isFileAvailable(message)) { - message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); - updateConversationUi(); - } - return; - } + Message message = conversation.findMessageWithFileAndUuid(uuid); + if (message != null) { + if (!getFileBackend().isFileAvailable(message)) { + message.setDownloadable(new DownloadablePlaceholder(Downloadable.STATUS_DELETED)); + updateConversationUi(); + } + return; } } } @@ -952,22 +958,43 @@ public class XmppConnectionService extends Service { }); } - public int loadMoreMessages(Conversation conversation, long timestamp) { - List<Message> messages = databaseBackend.getMessages(conversation, 50, - timestamp); - for (Message message : messages) { - message.setConversation(conversation); + 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)); + if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation,callback)) { + return; } - conversation.addAll(0, messages); - return messages.size(); + new Thread(new Runnable() { + @Override + public void run() { + final Account account = conversation.getAccount(); + List<Message> messages = databaseBackend.getMessages(conversation, 50,timestamp); + if (messages.size() > 0) { + conversation.addAll(0, messages); + callback.onMoreMessagesLoaded(messages.size(), conversation); + } else if (conversation.hasMessagesLeftOnServer() + && account.isOnlineAndConnected() + && account.getXmppConnection().getFeatures().mam()) { + MessageArchiveService.Query query = getMessageArchiveService().query(conversation,0,timestamp - 1); + if (query != null) { + query.setCallback(callback); + } + callback.informUser(R.string.fetching_history_from_server); + } + } + }).start(); + } + + public interface OnMoreMessagesLoaded { + public void onMoreMessagesLoaded(int count,Conversation conversation); + public void informUser(int r); } public List<Account> getAccounts() { return this.accounts; } - public Conversation find(List<Conversation> haystack, Contact contact) { - for (Conversation conversation : haystack) { + public Conversation find(final Iterable<Conversation> haystack, final Contact contact) { + for (final Conversation conversation : haystack) { if (conversation.getContact() == contact) { return conversation; } @@ -975,23 +1002,24 @@ public class XmppConnectionService extends Service { return null; } - public Conversation find(final List<Conversation> haystack, - final Account account, - final Jid jid) { + public Conversation find(final Iterable<Conversation> haystack, final Account account, final Jid jid) { if (jid == null ) { return null; } - for (Conversation conversation : haystack) { + for (final Conversation conversation : haystack) { if ((account == null || conversation.getAccount() == account) - && (conversation.getContactJid().toBareJid().equals(jid.toBareJid()))) { + && (conversation.getJid().toBareJid().equals(jid.toBareJid()))) { return conversation; } } return null; } - public Conversation findOrCreateConversation(final Account account, final Jid jid, - final boolean muc) { + public Conversation findOrCreateConversation(final Account account, final Jid jid,final boolean muc) { + return this.findOrCreateConversation(account,jid,muc,null); + } + + public Conversation findOrCreateConversation(final Account account, final Jid jid,final boolean muc, final MessageArchiveService.Query query) { synchronized (this.conversations) { Conversation conversation = find(account, jid); if (conversation != null) { @@ -1006,7 +1034,7 @@ public class XmppConnectionService extends Service { } else { conversation.setMode(Conversation.MODE_SINGLE); } - conversation.addAll(0, databaseBackend.getMessages(conversation, 50)); + conversation.addAll(0, databaseBackend.getMessages(conversation, Config.PAGE_SIZE)); this.databaseBackend.updateConversation(conversation); } else { String conversationName; @@ -1025,6 +1053,13 @@ public class XmppConnectionService extends Service { } this.databaseBackend.createConversation(conversation); } + if (query == null) { + this.mMessageArchiveService.query(conversation); + } else { + if (query.getConversation() == null) { + this.mMessageArchiveService.query(conversation,query.getStart()); + } + } this.conversations.add(conversation); updateConversationUi(); return conversation; @@ -1051,13 +1086,7 @@ public class XmppConnectionService extends Service { } } - public void clearConversationHistory(Conversation conversation) { - this.databaseBackend.deleteMessagesInConversation(conversation); - conversation.getMessages().clear(); - updateConversationUi(); - } - - public void createAccount(Account account) { + public void createAccount(final Account account) { account.initOtrEngine(this); databaseBackend.createAccount(account); this.accounts.add(account); @@ -1065,7 +1094,7 @@ public class XmppConnectionService extends Service { updateAccountUi(); } - public void updateAccount(Account account) { + public void updateAccount(final Account account) { this.statusListener.onStatusChanged(account); databaseBackend.updateAccount(account); reconnectAccount(account, false); @@ -1073,9 +1102,30 @@ public class XmppConnectionService extends Service { getNotificationService().updateErrorNotification(); } - public void deleteAccount(Account account) { + public void updateAccountPasswordOnServer(final Account account, final String newPassword, final OnAccountPasswordChanged callback) { + final IqPacket iq = getIqGenerator().generateSetPassword(account, newPassword); + sendIqPacket(account, iq, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.setPassword(newPassword); + databaseBackend.updateAccount(account); + callback.onPasswordChangeSucceeded(); + } else { + callback.onPasswordChangeFailed(); + } + } + }); + } + + public interface OnAccountPasswordChanged { + public void onPasswordChangeSucceeded(); + public void onPasswordChangeFailed(); + } + + public void deleteAccount(final Account account) { synchronized (this.conversations) { - for (Conversation conversation : conversations) { + for (final Conversation conversation : conversations) { if (conversation.getAccount() == account) { if (conversation.getMode() == Conversation.MODE_MULTI) { leaveMuc(conversation); @@ -1147,7 +1197,7 @@ public class XmppConnectionService extends Service { } } - public void setOnRosterUpdateListener(OnRosterUpdate listener) { + public void setOnRosterUpdateListener(final OnRosterUpdate listener) { synchronized (this) { if (checkListeners()) { switchToForeground(); @@ -1172,6 +1222,31 @@ public class XmppConnectionService extends Service { } } + public void setOnUpdateBlocklistListener(final OnUpdateBlocklist listener) { + synchronized (this) { + if (checkListeners()) { + switchToForeground(); + } + this.mOnUpdateBlocklist = listener; + if (this.updateBlocklistListenerCount < 2) { + this.updateBlocklistListenerCount++; + } + } + } + + public void removeOnUpdateBlocklistListener() { + synchronized (this) { + this.updateBlocklistListenerCount--; + if (this.updateBlocklistListenerCount <= 0) { + this.updateBlocklistListenerCount = 0; + this.mOnUpdateBlocklist = null; + if (checkListeners()) { + switchToBackground(); + } + } + } + } + public void setOnMucRosterUpdateListener(OnMucRosterUpdate listener) { synchronized (this) { if (checkListeners()) { @@ -1199,7 +1274,9 @@ public class XmppConnectionService extends Service { private boolean checkListeners() { return (this.mOnAccountUpdate == null - && this.mOnConversationUpdate == null && this.mOnRosterUpdate == null); + && this.mOnConversationUpdate == null + && this.mOnRosterUpdate == null + && this.mOnUpdateBlocklist == null); } private void switchToForeground() { @@ -1227,11 +1304,12 @@ public class XmppConnectionService extends Service { Log.d(Config.LOGTAG, "app switched into background"); } - public void connectMultiModeConversations(Account account) { + private void connectMultiModeConversations(Account account) { List<Conversation> conversations = getConversations(); for (Conversation conversation : conversations) { if ((conversation.getMode() == Conversation.MODE_MULTI) && (conversation.getAccount() == account)) { + conversation.resetMucOptions(); joinMuc(conversation); } } @@ -1251,29 +1329,18 @@ public class XmppConnectionService extends Service { PresencePacket packet = new PresencePacket(); packet.setFrom(conversation.getAccount().getJid()); packet.setTo(joinJid); - Element x = new Element("x"); - x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); + Element x = packet.addChild("x","http://jabber.org/protocol/muc"); if (conversation.getMucOptions().getPassword() != null) { - Element password = x.addChild("password"); - password.setContent(conversation.getMucOptions().getPassword()); + x.addChild("password").setContent(conversation.getMucOptions().getPassword()); } + x.addChild("history").setAttribute("since",PresenceGenerator.getTimestamp(conversation.getLastMessageTransmitted())); String sig = account.getPgpSignature(); if (sig != null) { packet.addChild("status").setContent("online"); packet.addChild("x", "jabber:x:signed").setContent(sig); } - if (conversation.getMessages().size() != 0) { - final SimpleDateFormat mDateFormat = new SimpleDateFormat( - "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US); - mDateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); - Date date = new Date(conversation.getLatestMessage() - .getTimeSent() + 1000); - x.addChild("history").setAttribute("since", - mDateFormat.format(date)); - } - packet.addChild(x); sendPresencePacket(account, packet); - if (!joinJid.equals(conversation.getContactJid())) { + if (!joinJid.equals(conversation.getJid())) { conversation.setContactJid(joinJid); databaseBackend.updateConversation(conversation); } @@ -1315,7 +1382,7 @@ public class XmppConnectionService extends Service { @Override public void onFailure() { - callback.error(R.string.nick_in_use,conversation); + callback.error(R.string.nick_in_use, conversation); } }); @@ -1349,14 +1416,14 @@ public class XmppConnectionService extends Service { account.pendingConferenceLeaves.remove(conversation); if (account.getStatus() == Account.State.ONLINE) { PresencePacket packet = new PresencePacket(); - packet.setTo(conversation.getContactJid()); + packet.setTo(conversation.getJid()); packet.setFrom(conversation.getAccount().getJid()); packet.setAttribute("type", "unavailable"); sendPresencePacket(conversation.getAccount(), packet); conversation.getMucOptions().setOffline(); conversation.deregisterWithBookmark(); Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid() - + ": leaving muc " + conversation.getContactJid()); + + ": leaving muc " + conversation.getJid()); } else { account.pendingConferenceLeaves.add(conversation); } @@ -1381,8 +1448,8 @@ public class XmppConnectionService extends Service { return null; } - public void createAdhocConference(final Account account, final List<Jid> jids, final UiCallback<Conversation> callback) { - Log.d(Config.LOGTAG,account.getJid().toBareJid().toString()+": creating adhoc conference with "+ jids.toString()); + public void createAdhocConference(final Account account, final Iterable<Jid> jids, final UiCallback<Conversation> callback) { + Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": creating adhoc conference with " + jids.toString()); if (account.getStatus() == Account.State.ONLINE) { try { String server = findConferenceServer(account); @@ -1434,7 +1501,7 @@ public class XmppConnectionService extends Service { public void pushConferenceConfiguration(final Conversation conversation,final Bundle options, final OnConferenceOptionsPushed callback) { IqPacket request = new IqPacket(IqPacket.TYPE_GET); - request.setTo(conversation.getContactJid().toBareJid()); + request.setTo(conversation.getJid().toBareJid()); request.query("http://jabber.org/protocol/muc#owner"); sendIqPacket(conversation.getAccount(),request,new OnIqPacketReceived() { @Override @@ -1448,7 +1515,7 @@ public class XmppConnectionService extends Service { } data.submit(); IqPacket set = new IqPacket(IqPacket.TYPE_SET); - set.setTo(conversation.getContactJid().toBareJid()); + set.setTo(conversation.getJid().toBareJid()); set.query("http://jabber.org/protocol/muc#owner").addChild(data); sendIqPacket(account, set, new OnIqPacketReceived() { @Override @@ -1486,7 +1553,7 @@ public class XmppConnectionService extends Service { if (conversation.endOtrIfNeeded()) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": ended otr session with " - + conversation.getContactJid()); + + conversation.getJid()); } } } @@ -1528,36 +1595,35 @@ public class XmppConnectionService extends Service { } public void onOtrSessionEstablished(Conversation conversation) { - Account account = conversation.getAccount(); - List<Message> messages = conversation.getMessages(); - Session otrSession = conversation.getOtrSession(); + final Account account = conversation.getAccount(); + final Session otrSession = conversation.getOtrSession(); Log.d(Config.LOGTAG, account.getJid().toBareJid() + " otr session established with " - + conversation.getContactJid() + "/" + + conversation.getJid() + "/" + otrSession.getSessionID().getUserID()); - for (Message msg : messages) { - if ((msg.getStatus() == Message.STATUS_UNSEND || msg.getStatus() == Message.STATUS_WAITING) - && (msg.getEncryption() == Message.ENCRYPTION_OTR)) { + conversation.findUnsentMessagesWithOtrEncryption(new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { SessionID id = otrSession.getSessionID(); try { - msg.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID())); + message.setCounterpart(Jid.fromString(id.getAccountID() + "/" + id.getUserID())); } catch (InvalidJidException e) { - break; + return; } - if (msg.getType() == Message.TYPE_TEXT) { - MessagePacket outPacket = mMessageGenerator - .generateOtrChat(msg, true); + if (message.getType() == Message.TYPE_TEXT) { + MessagePacket outPacket = mMessageGenerator.generateOtrChat(message, true); if (outPacket != null) { - msg.setStatus(Message.STATUS_SEND); - databaseBackend.updateMessage(msg); + message.setStatus(Message.STATUS_SEND); + databaseBackend.updateMessage(message); sendMessagePacket(account, outPacket); } - } else if (msg.getType() == Message.TYPE_IMAGE || msg.getType() == Message.TYPE_FILE) { - mJingleConnectionManager.createNewConnection(msg); + } else if (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) { + mJingleConnectionManager.createNewConnection(message); } - } - } - updateConversationUi(); + updateConversationUi(); + } + }); } public boolean renewSymmetricKey(Conversation conversation) { @@ -1587,17 +1653,17 @@ public class XmppConnectionService extends Service { return false; } - public void pushContactToServer(Contact contact) { + public void pushContactToServer(final Contact contact) { contact.resetOption(Contact.Options.DIRTY_DELETE); contact.setOption(Contact.Options.DIRTY_PUSH); - Account account = contact.getAccount(); + final Account account = contact.getAccount(); if (account.getStatus() == Account.State.ONLINE) { - boolean ask = contact.getOption(Contact.Options.ASKING); - boolean sendUpdates = contact + final boolean ask = contact.getOption(Contact.Options.ASKING); + final boolean sendUpdates = contact .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST) && contact.getOption(Contact.Options.PREEMPTIVE_GRANT); - IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - iq.query("jabber:iq:roster").addChild(contact.asElement()); + final IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + iq.query(Xmlns.ROSTER).addChild(contact.asElement()); account.getXmppConnection().sendIqPacket(iq, null); if (sendUpdates) { sendPresencePacket(account, @@ -1610,7 +1676,8 @@ public class XmppConnectionService extends Service { } } - public void publishAvatar(Account account, Uri image, + public void publishAvatar(final Account account, + final Uri image, final UiCallback<Avatar> callback) { final Bitmap.CompressFormat format = Config.AVATAR_FORMAT; final int size = Config.AVATAR_SIZE; @@ -1630,13 +1697,13 @@ public class XmppConnectionService extends Service { callback.error(R.string.error_saving_avatar, avatar); return; } - IqPacket packet = this.mIqGenerator.publishAvatar(avatar); + final IqPacket packet = this.mIqGenerator.publishAvatar(avatar); this.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket result) { if (result.getType() == IqPacket.TYPE_RESULT) { - IqPacket packet = XmppConnectionService.this.mIqGenerator + final IqPacket packet = XmppConnectionService.this.mIqGenerator .publishAvatarMetadata(avatar); sendIqPacket(account, packet, new OnIqPacketReceived() { @@ -1679,7 +1746,7 @@ public class XmppConnectionService extends Service { @Override public void onIqPacketReceived(Account account, IqPacket result) { final String ERROR = account.getJid().toBareJid() - + ": fetching avatar for " + avatar.owner + " failed "; + + ": fetching avatar for " + avatar.owner + " failed "; if (result.getType() == IqPacket.TYPE_RESULT) { avatar.image = mIqParser.avatarData(result); if (avatar.image != null) { @@ -1693,7 +1760,7 @@ public class XmppConnectionService extends Service { updateAccountUi(); } else { Contact contact = account.getRoster() - .getContact(avatar.owner); + .getContact(avatar.owner); contact.setAvatar(avatar.getFilename()); getAvatarService().clear(contact); updateConversationUi(); @@ -1769,7 +1836,7 @@ public class XmppConnectionService extends Service { Account account = contact.getAccount(); if (account.getStatus() == Account.State.ONLINE) { IqPacket iq = new IqPacket(IqPacket.TYPE_SET); - Element item = iq.query("jabber:iq:roster").addChild("item"); + Element item = iq.query(Xmlns.ROSTER).addChild("item"); item.setAttribute("jid", contact.getJid().toString()); item.setAttribute("subscription", "remove"); account.getXmppConnection().sendIqPacket(iq, null); @@ -1812,12 +1879,13 @@ public class XmppConnectionService extends Service { public void resetSendingToWaiting(Account account) { for (Conversation conversation : getConversations()) { if (conversation.getAccount() == account) { - for (Message message : conversation.getMessages()) { - if (message.getType() != Message.TYPE_IMAGE - && message.getStatus() == Message.STATUS_UNSEND) { + conversation.findUnsentTextMessages(new Conversation.OnMessageFound() { + + @Override + public void onMessageFound(Message message) { markMessage(message, Message.STATUS_WAITING); - } - } + } + }); } } } @@ -1828,7 +1896,7 @@ public class XmppConnectionService extends Service { return false; } else { for (Conversation conversation : getConversations()) { - if (conversation.getContactJid().equals(recipient) + if (conversation.getJid().equals(recipient) && conversation.getAccount().equals(account)) { return markMessage(conversation, uuid, status); } @@ -1842,15 +1910,13 @@ public class XmppConnectionService extends Service { if (uuid == null) { return false; } else { - for (Message message : conversation.getMessages()) { - if (uuid.equals(message.getUuid()) - || (message.getStatus() >= Message.STATUS_SEND && uuid - .equals(message.getRemoteMsgId()))) { - markMessage(message, status); - return true; - } + Message message = conversation.findSentMessageWithUuid(uuid); + if (message!=null) { + markMessage(message,status); + return true; + } else { + return false; } - return false; } } @@ -1904,6 +1970,12 @@ public class XmppConnectionService extends Service { } } + public void updateBlocklistUi(final OnUpdateBlocklist.Status status) { + if (mOnUpdateBlocklist != null) { + mOnUpdateBlocklist.OnUpdateBlocklist(status); + } + } + public void updateMucRosterUi() { if (mOnMucRosterUpdate != null) { mOnMucRosterUpdate.onMucRosterUpdate(); @@ -1928,29 +2000,22 @@ public class XmppConnectionService extends Service { return null; } - public void markRead(Conversation conversation, boolean calledByUi) { + public void markRead(final Conversation conversation) { mNotificationService.clear(conversation); - final Message markable = conversation.getLatestMarkableMessage(); conversation.markRead(); - if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null && calledByUi) { + } + + public void sendReadMarker(final Conversation conversation) { + final Message markable = conversation.getLatestMarkableMessage(); + this.markRead(conversation); + if (confirmMessages() && markable != null && markable.getRemoteMsgId() != null) { Log.d(Config.LOGTAG, conversation.getAccount().getJid().toBareJid()+ ": sending read marker to " + markable.getCounterpart().toString()); Account account = conversation.getAccount(); final Jid to = markable.getCounterpart(); MessagePacket packet = mMessageGenerator.confirm(account, to, markable.getRemoteMsgId()); this.sendMessagePacket(conversation.getAccount(),packet); } - if (!calledByUi) { - updateConversationUi(); - } - } - - public void failWaitingOtrMessages(Conversation conversation) { - for (Message message : conversation.getMessages()) { - if (message.getEncryption() == Message.ENCRYPTION_OTR - && message.getStatus() == Message.STATUS_WAITING) { - markMessage(message, Message.STATUS_SEND_FAILED); - } - } + updateConversationUi(); } public SecureRandom getRNG() { @@ -1969,14 +2034,6 @@ public class XmppConnectionService extends Service { return this.mBitmapCache; } - public void replyWithNotAcceptable(Account account, MessagePacket packet) { - if (account.getStatus() == Account.State.ONLINE) { - MessagePacket error = this.mMessageGenerator - .generateNotAcceptable(packet); - sendMessagePacket(account, error); - } - } - public void syncRosterToDisk(final Account account) { new Thread(new Runnable() { @@ -1989,12 +2046,12 @@ public class XmppConnectionService extends Service { } public List<String> getKnownHosts() { - List<String> hosts = new ArrayList<>(); - for (Account account : getAccounts()) { + final List<String> hosts = new ArrayList<>(); + for (final Account account : getAccounts()) { if (!hosts.contains(account.getServer().toString())) { hosts.add(account.getServer().toString()); } - for (Contact contact : account.getRoster().getContacts()) { + for (final Contact contact : account.getRoster().getContacts()) { if (contact.showInRoster()) { final String server = contact.getServer().toString(); if (server != null && !hosts.contains(server)) { @@ -2007,10 +2064,10 @@ public class XmppConnectionService extends Service { } public List<String> getKnownConferenceHosts() { - ArrayList<String> mucServers = new ArrayList<>(); - for (Account account : accounts) { + final ArrayList<String> mucServers = new ArrayList<>(); + for (final Account account : accounts) { if (account.getXmppConnection() != null) { - String server = account.getXmppConnection().getMucServer(); + final String server = account.getXmppConnection().getMucServer(); if (server != null && !mucServers.contains(server)) { mucServers.add(server); } @@ -2033,9 +2090,8 @@ public class XmppConnectionService extends Service { } } - public void sendIqPacket(Account account, IqPacket packet, - OnIqPacketReceived callback) { - XmppConnection connection = account.getXmppConnection(); + public void sendIqPacket(final Account account, final IqPacket packet, final PacketReceived callback) { + final XmppConnection connection = account.getXmppConnection(); if (connection != null) { connection.sendIqPacket(packet, callback); } @@ -2053,10 +2109,16 @@ public class XmppConnectionService extends Service { return this.mIqGenerator; } + public IqParser getIqParser() { return this.mIqParser; } + public JingleConnectionManager getJingleConnectionManager() { return this.mJingleConnectionManager; } + public MessageArchiveService getMessageArchiveService() { + return this.mMessageArchiveService; + } + public List<Contact> findContacts(Jid jid) { ArrayList<Contact> contacts = new ArrayList<>(); for (Account account : getAccounts()) { @@ -2078,8 +2140,8 @@ public class XmppConnectionService extends Service { return this.mHttpConnectionManager; } - public void resendFailedMessages(Message message) { - List<Message> messages = new ArrayList<>(); + public void resendFailedMessages(final Message message) { + final Collection<Message> messages = new ArrayList<>(); Message current = message; while (current.getStatus() == Message.STATUS_SEND_FAILED) { messages.add(current); @@ -2089,12 +2151,23 @@ public class XmppConnectionService extends Service { break; } } - for (Message msg : messages) { + for (final Message msg : messages) { markMessage(msg, Message.STATUS_WAITING); this.resendMessage(msg); } } + public void clearConversationHistory(final Conversation conversation) { + conversation.clearMessages(); + conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam + new Thread(new Runnable() { + @Override + public void run() { + databaseBackend.deleteMessagesInConversation(conversation); + } + }).start(); + } + public interface OnConversationUpdate { public void onConversationUpdate(); } @@ -2121,4 +2194,35 @@ public class XmppConnectionService extends Service { return XmppConnectionService.this; } } + + public void sendBlockRequest(final Blockable blockable) { + if (blockable != null && blockable.getBlockedJid() != null) { + final Jid jid = blockable.getBlockedJid(); + this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetBlockRequest(jid), new OnIqPacketReceived() { + + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.getBlocklist().add(jid); + updateBlocklistUi(OnUpdateBlocklist.Status.BLOCKED); + } + } + }); + } + } + + public void sendUnblockRequest(final Blockable blockable) { + if (blockable != null && blockable.getJid() != null) { + final Jid jid = blockable.getBlockedJid(); + this.sendIqPacket(blockable.getAccount(), getIqGenerator().generateSetUnblockRequest(jid), new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(final Account account, final IqPacket packet) { + if (packet.getType() == IqPacket.TYPE_RESULT) { + account.getBlocklist().remove(jid); + updateBlocklistUi(OnUpdateBlocklist.Status.UNBLOCKED); + } + } + }); + } + } } diff --git a/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java b/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java new file mode 100644 index 00000000..1a9fc95c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/AbstractSearchableListItemActivity.java @@ -0,0 +1,124 @@ +package eu.siacs.conversations.ui; + +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ListView; + +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.ListItem; +import eu.siacs.conversations.ui.adapter.ListItemAdapter; + +public abstract class AbstractSearchableListItemActivity extends XmppActivity { + private ListView mListView; + private final List<ListItem> listItems = new ArrayList<>(); + private ArrayAdapter<ListItem> mListItemsAdapter; + + private EditText mSearchEditText; + + private final MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { + + @Override + public boolean onMenuItemActionExpand(final MenuItem item) { + mSearchEditText.post(new Runnable() { + + @Override + public void run() { + mSearchEditText.requestFocus(); + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(mSearchEditText, + InputMethodManager.SHOW_IMPLICIT); + } + }); + + return true; + } + + @Override + public boolean onMenuItemActionCollapse(final MenuItem item) { + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + InputMethodManager.HIDE_IMPLICIT_ONLY); + mSearchEditText.setText(""); + filterContacts(); + return true; + } + }; + + private final TextWatcher mSearchTextWatcher = new TextWatcher() { + + @Override + public void afterTextChanged(final Editable editable) { + filterContacts(editable.toString()); + } + + @Override + public void beforeTextChanged(final CharSequence s, final int start, final int count, + final int after) { + } + + @Override + public void onTextChanged(final CharSequence s, final int start, final int before, + final int count) { + } + }; + + public ListView getListView() { + return mListView; + } + + public List<ListItem> getListItems() { + return listItems; + } + + public EditText getSearchEditText() { + return mSearchEditText; + } + + public ArrayAdapter<ListItem> getListItemAdapter() { + return mListItemsAdapter; + } + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_choose_contact); + mListView = (ListView) findViewById(R.id.choose_contact_list); + mListView.setFastScrollEnabled(true); + mListItemsAdapter = new ListItemAdapter(this, listItems); + mListView.setAdapter(mListItemsAdapter); + } + + @Override + public boolean onCreateOptionsMenu(final Menu menu) { + getMenuInflater().inflate(R.menu.choose_contact, menu); + final MenuItem menuSearchView = menu.findItem(R.id.action_search); + final View mSearchView = menuSearchView.getActionView(); + mSearchEditText = (EditText) mSearchView + .findViewById(R.id.search_field); + mSearchEditText.addTextChangedListener(mSearchTextWatcher); + menuSearchView.setOnActionExpandListener(mOnActionExpandListener); + return true; + } + + protected void filterContacts() { + filterContacts(null); + } + + protected abstract void filterContacts(final String needle); + + @Override + void onBackendConnected() { + filterContacts(); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java new file mode 100644 index 00000000..9cf7e9f8 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/BlockContactDialog.java @@ -0,0 +1,41 @@ +package eu.siacs.conversations.ui; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Blockable; +import eu.siacs.conversations.services.XmppConnectionService; + +public final class BlockContactDialog { + public static void show(final Context context, + final XmppConnectionService xmppConnectionService, + final Blockable blockable) { + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + final boolean isBlocked = blockable.isBlocked(); + builder.setNegativeButton(R.string.cancel, null); + + if (blockable.getJid().isDomainJid() || blockable.getAccount().isBlocked(blockable.getJid().toDomainJid())) { + builder.setTitle(isBlocked ? R.string.action_unblock_domain : R.string.action_block_domain); + builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_domain_text : R.string.block_domain_text, + blockable.getJid().toDomainJid())); + } else { + builder.setTitle(isBlocked ? R.string.action_unblock_contact : R.string.action_block_contact); + builder.setMessage(context.getResources().getString(isBlocked ? R.string.unblock_contact_text : R.string.block_contact_text, + blockable.getJid().toBareJid())); + } + builder.setPositiveButton(isBlocked ? R.string.unblock : R.string.block, new DialogInterface.OnClickListener() { + + @Override + public void onClick(final DialogInterface dialog, final int which) { + if (isBlocked) { + xmppConnectionService.sendUnblockRequest(blockable); + } else { + xmppConnectionService.sendBlockRequest(blockable); + } + } + }); + builder.create().show(); + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java new file mode 100644 index 00000000..13d7f4fc --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/BlocklistActivity.java @@ -0,0 +1,75 @@ +package eu.siacs.conversations.ui; + +import android.os.Bundle; +import android.text.Editable; +import android.view.View; +import android.widget.AdapterView; + +import java.util.Collections; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Contact; +import eu.siacs.conversations.xmpp.OnUpdateBlocklist; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class BlocklistActivity extends AbstractSearchableListItemActivity implements OnUpdateBlocklist { + + private Account account = null; + + @Override + public void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { + + @Override + public boolean onItemLongClick(final AdapterView<?> parent, + final View view, + final int position, + final long id) { + BlockContactDialog.show(parent.getContext(), xmppConnectionService,(Contact) getListItems().get(position)); + return true; + } + }); + } + + @Override + public void onBackendConnected() { + for (final Account account : xmppConnectionService.getAccounts()) { + if (account.getJid().toString().equals(getIntent().getStringExtra("account"))) { + this.account = account; + break; + } + } + filterContacts(); + } + + @Override + protected void filterContacts(final String needle) { + getListItems().clear(); + if (account != null) { + for (final Jid jid : account.getBlocklist()) { + final Contact contact = account.getRoster().getContact(jid); + if (contact.match(needle) && contact.isBlocked()) { + getListItems().add(contact); + } + } + Collections.sort(getListItems()); + } + runOnUiThread(new Runnable() { + @Override + public void run() { + getListItemAdapter().notifyDataSetChanged(); + } + }); + } + + @Override + public void OnUpdateBlocklist(final OnUpdateBlocklist.Status status) { + final Editable editable = getSearchEditText().getText(); + if (editable != null) { + filterContacts(editable.toString()); + } else { + filterContacts(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java new file mode 100644 index 00000000..54c064c6 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/ChangePasswordActivity.java @@ -0,0 +1,107 @@ +package eu.siacs.conversations.ui; + +import android.os.Bundle; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + +public class ChangePasswordActivity extends XmppActivity implements XmppConnectionService.OnAccountPasswordChanged { + + private Button mChangePasswordButton; + private View.OnClickListener mOnChangePasswordButtonClicked = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mAccount != null) { + final String currentPassword = mCurrentPassword.getText().toString(); + final String newPassword = mNewPassword.getText().toString(); + final String newPasswordConfirm = mNewPasswordConfirm.getText().toString(); + if (!currentPassword.equals(mAccount.getPassword())) { + mCurrentPassword.requestFocus(); + mCurrentPassword.setError(getString(R.string.account_status_unauthorized)); + } else if (!newPassword.equals(newPasswordConfirm)) { + mNewPasswordConfirm.requestFocus(); + mNewPasswordConfirm.setError(getString(R.string.passwords_do_not_match)); + } else if (newPassword.trim().isEmpty()) { + mNewPassword.requestFocus(); + mNewPassword.setError(getString(R.string.password_should_not_be_empty)); + } else { + mCurrentPassword.setError(null); + mNewPassword.setError(null); + mNewPasswordConfirm.setError(null); + xmppConnectionService.updateAccountPasswordOnServer(mAccount, newPassword, ChangePasswordActivity.this); + mChangePasswordButton.setEnabled(false); + mChangePasswordButton.setTextColor(getSecondaryTextColor()); + mChangePasswordButton.setText(R.string.updating); + } + } + } + }; + private EditText mCurrentPassword; + private EditText mNewPassword; + private EditText mNewPasswordConfirm; + private Account mAccount; + + @Override + void onBackendConnected() { + try { + final String jid = getIntent() == null ? null : getIntent().getStringExtra("account"); + if (jid != null) { + this.mAccount = xmppConnectionService.findAccountByJid(Jid.fromString(jid)); + } + } catch (final InvalidJidException ignored) { + + } + + } + + @Override + protected void onCreate(final Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_change_password); + Button mCancelButton = (Button) findViewById(R.id.left_button); + mCancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + finish(); + } + }); + this.mChangePasswordButton = (Button) findViewById(R.id.right_button); + this.mChangePasswordButton.setOnClickListener(this.mOnChangePasswordButtonClicked); + this.mCurrentPassword = (EditText) findViewById(R.id.current_password); + this.mNewPassword = (EditText) findViewById(R.id.new_password); + this.mNewPasswordConfirm = (EditText) findViewById(R.id.new_password_confirm); + } + + @Override + public void onPasswordChangeSucceeded() { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(ChangePasswordActivity.this,R.string.password_changed,Toast.LENGTH_LONG).show(); + finish(); + } + }); + } + + @Override + public void onPasswordChangeFailed() { + runOnUiThread(new Runnable() { + @Override + public void run() { + mNewPassword.setError(getString(R.string.could_not_change_password)); + mChangePasswordButton.setEnabled(true); + mChangePasswordButton.setTextColor(getPrimaryTextColor()); + mChangePasswordButton.setText(R.string.change_password); + } + }); + + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java index e7254933..70b353c6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ChooseContactActivity.java @@ -1,101 +1,33 @@ package eu.siacs.conversations.ui; -import java.util.ArrayList; -import java.util.Collections; - import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.EditText; -import android.widget.ListView; -import eu.siacs.conversations.R; + +import java.util.Collections; + import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.ui.adapter.ListItemAdapter; - -public class ChooseContactActivity extends XmppActivity { - - private ListView mListView; - private ArrayList<ListItem> contacts = new ArrayList<>(); - private ArrayAdapter<ListItem> mContactsAdapter; - - private EditText mSearchEditText; - - private TextWatcher mSearchTextWatcher = new TextWatcher() { - - @Override - public void afterTextChanged(Editable editable) { - filterContacts(editable.toString()); - } - - @Override - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, - int count) { - } - }; - - private MenuItem.OnActionExpandListener mOnActionExpandListener = new MenuItem.OnActionExpandListener() { - - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - mSearchEditText.post(new Runnable() { - - @Override - public void run() { - mSearchEditText.requestFocus(); - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(mSearchEditText, - InputMethodManager.SHOW_IMPLICIT); - } - }); - - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), - InputMethodManager.HIDE_IMPLICIT_ONLY); - mSearchEditText.setText(""); - filterContacts(null); - return true; - } - }; +public class ChooseContactActivity extends AbstractSearchableListItemActivity { @Override - public void onCreate(Bundle savedInstanceState) { + public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_choose_contact); - mListView = (ListView) findViewById(R.id.choose_contact_list); - mListView.setFastScrollEnabled(true); - mContactsAdapter = new ListItemAdapter(this, contacts); - mListView.setAdapter(mContactsAdapter); - mListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override - public void onItemClick(AdapterView<?> arg0, View arg1, - int position, long arg3) { - InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); - imm.hideSoftInputFromWindow(mSearchEditText.getWindowToken(), + public void onItemClick(final AdapterView<?> parent, final View view, + final int position, final long id) { + final InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(getSearchEditText().getWindowToken(), InputMethodManager.HIDE_IMPLICIT_ONLY); - Intent request = getIntent(); - Intent data = new Intent(); - ListItem mListItem = contacts.get(position); + final Intent request = getIntent(); + final Intent data = new Intent(); + final ListItem mListItem = getListItems().get(position); data.putExtra("contact", mListItem.getJid().toString()); String account = request.getStringExtra("account"); if (account == null && mListItem instanceof Contact) { @@ -108,38 +40,21 @@ public class ChooseContactActivity extends XmppActivity { finish(); } }); - } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.choose_contact, menu); - MenuItem menuSearchView = menu.findItem(R.id.action_search); - View mSearchView = menuSearchView.getActionView(); - mSearchEditText = (EditText) mSearchView - .findViewById(R.id.search_field); - mSearchEditText.addTextChangedListener(mSearchTextWatcher); - menuSearchView.setOnActionExpandListener(mOnActionExpandListener); - return true; } - @Override - void onBackendConnected() { - filterContacts(null); - } - - protected void filterContacts(String needle) { - this.contacts.clear(); - for (Account account : xmppConnectionService.getAccounts()) { + protected void filterContacts(final String needle) { + getListItems().clear(); + for (final Account account : xmppConnectionService.getAccounts()) { if (account.getStatus() != Account.State.DISABLED) { - for (Contact contact : account.getRoster().getContacts()) { + for (final Contact contact : account.getRoster().getContacts()) { if (contact.showInRoster() && contact.match(needle)) { - this.contacts.add(contact); + getListItems().add(contact); } } } } - Collections.sort(this.contacts); - mContactsAdapter.notifyDataSetChanged(); + Collections.sort(getListItems()); + getListItemAdapter().notifyDataSetChanged(); } - } diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 2e36c545..eeb015f3 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -157,8 +157,8 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers @Override public void onValueEdited(String value) { MessagePacket packet = xmppConnectionService - .getMessageGenerator().conferenceSubject( - mConversation, value); + .getMessageGenerator().conferenceSubject( + mConversation, value); xmppConnectionService.sendMessagePacket( mConversation.getAccount(), packet); } @@ -191,7 +191,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers @Override protected String getShareableUri() { if (mConversation != null) { - return "xmpp:" + mConversation.getContactJid().toBareJid().toString() + "?join"; + return "xmpp:" + mConversation.getJid().toBareJid().toString() + "?join"; } else { return ""; } @@ -202,7 +202,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers MenuItem menuItemSaveBookmark = menu.findItem(R.id.action_save_as_bookmark); MenuItem menuItemDeleteBookmark = menu.findItem(R.id.action_delete_bookmark); Account account = mConversation.getAccount(); - if (account.hasBookmarkFor(mConversation.getContactJid().toBareJid())) { + if (account.hasBookmarkFor(mConversation.getJid().toBareJid())) { menuItemSaveBookmark.setVisible(false); menuItemDeleteBookmark.setVisible(true); } else { @@ -263,9 +263,9 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers protected void saveAsBookmark() { Account account = mConversation.getAccount(); - Bookmark bookmark = new Bookmark(account, mConversation.getContactJid().toBareJid()); - if (!mConversation.getContactJid().isBareJid()) { - bookmark.setNick(mConversation.getContactJid().getResourcepart()); + Bookmark bookmark = new Bookmark(account, mConversation.getJid().toBareJid()); + if (!mConversation.getJid().isBareJid()) { + bookmark.setNick(mConversation.getJid().getResourcepart()); } bookmark.setAutojoin(true); account.getBookmarks().add(bookmark); @@ -288,7 +288,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers } if (uuid != null) { this.mConversation = xmppConnectionService - .findConversationByUuid(uuid); + .findConversationByUuid(uuid); if (this.mConversation != null) { populateView(); } @@ -297,11 +297,11 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers private void populateView() { mAccountJid.setText(getString(R.string.using_account, mConversation - .getAccount().getJid().toBareJid())); + .getAccount().getJid().toBareJid())); mYourPhoto.setImageBitmap(avatarService().get( - mConversation.getAccount(), getPixel(48))); + mConversation.getAccount(), getPixel(48))); setTitle(mConversation.getName()); - mFullJid.setText(mConversation.getContactJid().toBareJid().toString()); + mFullJid.setText(mConversation.getJid().toBareJid().toString()); mYourNick.setText(mConversation.getMucOptions().getActualNick()); mRoleAffiliaton = (TextView) findViewById(R.id.muc_role); if (mConversation.getMucOptions().online()) { @@ -338,7 +338,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers registerForContextMenu(view); view.setTag(user); TextView name = (TextView) view - .findViewById(R.id.contact_display_name); + .findViewById(R.id.contact_display_name); TextView key = (TextView) view.findViewById(R.id.key); TextView role = (TextView) view.findViewById(R.id.contact_jid); if (user.getPgpKeyId() != 0) { diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 4259371a..657ae75b 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -37,11 +37,13 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.UIHelper; +import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate { +public class ContactDetailsActivity extends XmppActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist { public static final String ACTION_VIEW_CONTACT = "view_contact"; private Contact contact; @@ -50,7 +52,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd @Override public void onClick(DialogInterface dialog, int which) { ContactDetailsActivity.this.xmppConnectionService - .deleteContactOnServer(contact); + .deleteContactOnServer(contact); ContactDetailsActivity.this.finish(); } }; @@ -58,14 +60,14 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd @Override public void onCheckedChanged(CompoundButton buttonView, - boolean isChecked) { + boolean isChecked) { if (isChecked) { if (contact .getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) { xmppConnectionService.sendPresencePacket(contact - .getAccount(), + .getAccount(), xmppConnectionService.getPresenceGenerator() - .sendPresenceUpdatesTo(contact)); + .sendPresenceUpdatesTo(contact)); } else { contact.setOption(Contact.Options.PREEMPTIVE_GRANT); } @@ -73,7 +75,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd contact.resetOption(Contact.Options.PREEMPTIVE_GRANT); xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator() - .stopPresenceUpdatesTo(contact)); + .stopPresenceUpdatesTo(contact)); } } }; @@ -81,15 +83,15 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd @Override public void onCheckedChanged(CompoundButton buttonView, - boolean isChecked) { + boolean isChecked) { if (isChecked) { xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator() - .requestPresenceUpdatesFrom(contact)); + .requestPresenceUpdatesFrom(contact)); } else { xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator() - .stopPresenceUpdatesFrom(contact)); + .stopPresenceUpdatesFrom(contact)); } } }; @@ -127,7 +129,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd ContactDetailsActivity.this); builder.setTitle(getString(R.string.action_add_phone_book)); builder.setMessage(getString(R.string.add_phone_book_text, - contact.getJid())); + contact.getJid())); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton(getString(R.string.add), addToPhonebook); builder.create().show(); @@ -166,7 +168,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) { try { @@ -188,15 +190,17 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd badge = (QuickContactBadge) findViewById(R.id.details_contact_badge); keys = (LinearLayout) findViewById(R.id.details_contact_keys); tags = (LinearLayout) findViewById(R.id.tags); - getActionBar().setHomeButtonEnabled(true); - getActionBar().setDisplayHomeAsUpEnabled(true); + if (getActionBar() != null) { + getActionBar().setHomeButtonEnabled(true); + getActionBar().setDisplayHomeAsUpEnabled(true); + } - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); this.showDynamicTags = preferences.getBoolean("show_dynamic_tags",false); } @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { + public boolean onOptionsItemSelected(final MenuItem menuItem) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setNegativeButton(getString(R.string.cancel), null); switch (menuItem.getItemId()) { @@ -205,11 +209,11 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd break; case R.id.action_delete_contact: builder.setTitle(getString(R.string.action_delete_contact)) - .setMessage( - getString(R.string.remove_contact_text, - contact.getJid())) - .setPositiveButton(getString(R.string.delete), - removeFromRoster).create().show(); + .setMessage( + getString(R.string.remove_contact_text, + contact.getJid())) + .setPositiveButton(getString(R.string.delete), + removeFromRoster).create().show(); break; case R.id.action_edit_contact: if (contact.getSystemAccount() == null) { @@ -219,7 +223,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd public void onValueEdited(String value) { contact.setServerName(value); ContactDetailsActivity.this.xmppConnectionService - .pushContactToServer(contact); + .pushContactToServer(contact); populateView(); } }); @@ -285,7 +289,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange); lastseen.setText(UIHelper.lastseen(getApplicationContext(), - contact.lastseen.time)); + contact.lastseen.time)); if (contact.getPresences().size() > 1) { contactJidTv.setText(contact.getJid() + " (" @@ -294,7 +298,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd contactJidTv.setText(contact.getJid().toString()); } accountJidTv.setText(getString(R.string.using_account, contact - .getAccount().getJid().toBareJid())); + .getAccount().getJid().toBareJid())); prepareContactBadge(badge, contact); if (contact.getSystemAccount() == null) { badge.setOnClickListener(onBadgeClick); @@ -309,10 +313,10 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd TextView key = (TextView) view.findViewById(R.id.key); TextView keyType = (TextView) view.findViewById(R.id.key_type); ImageButton remove = (ImageButton) view - .findViewById(R.id.button_remove); + .findViewById(R.id.button_remove); remove.setVisibility(View.VISIBLE); keyType.setText("OTR Fingerprint"); - key.setText(otrFingerprint); + key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint)); keys.addView(view); remove.setOnClickListener(new OnClickListener() { @@ -334,7 +338,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd @Override public void onClick(View v) { PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService - .getPgpEngine(); + .getPgpEngine(); if (pgp != null) { PendingIntent intent = pgp.getIntentForKey(contact); if (intent != null) { @@ -363,8 +367,8 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd } else { tags.setVisibility(View.VISIBLE); tags.removeAllViewsInLayout(); - for(ListItem.Tag tag : tagList) { - TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tags,false); + for(final ListItem.Tag tag : tagList) { + final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tags,false); tv.setText(tag.getName()); tv.setBackgroundColor(tag.getColor()); tags.addView(tv); @@ -393,8 +397,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd public void onClick(DialogInterface dialog, int which) { if (contact.deleteOtrFingerprint(fingerprint)) { populateView(); - xmppConnectionService.syncRosterToDisk(contact - .getAccount()); + xmppConnectionService.syncRosterToDisk(contact.getAccount()); } } @@ -406,7 +409,7 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd public void onBackendConnected() { if ((accountJid != null) && (contactJid != null)) { Account account = xmppConnectionService - .findAccountByJid(accountJid); + .findAccountByJid(accountJid); if (account == null) { return; } @@ -414,4 +417,15 @@ public class ContactDetailsActivity extends XmppActivity implements OnAccountUpd populateView(); } } + + @Override + public void OnUpdateBlocklist(final Status status) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + populateView(); + } + }); + } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 5b0fa562..5cfc1116 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -9,13 +9,13 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.IntentSender.SendIntentException; +import android.media.MediaActionSound; import android.net.Uri; import android.os.Bundle; import android.os.SystemClock; import android.provider.MediaStore; import android.support.v4.widget.SlidingPaneLayout; import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener; -import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.View; @@ -28,10 +28,14 @@ import android.widget.PopupMenu; import android.widget.PopupMenu.OnMenuItemClickListener; import android.widget.Toast; +import net.java.otr4j.session.SessionStatus; + import java.util.ArrayList; import java.util.List; import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; @@ -40,27 +44,23 @@ import eu.siacs.conversations.services.XmppConnectionService.OnConversationUpdat import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.ui.adapter.ConversationAdapter; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.xmpp.OnUpdateBlocklist; -public class ConversationActivity extends XmppActivity implements - OnAccountUpdate, OnConversationUpdate, OnRosterUpdate { +public class ConversationActivity extends XmppActivity + implements OnAccountUpdate, OnConversationUpdate, OnRosterUpdate, OnUpdateBlocklist { public static final String VIEW_CONVERSATION = "viewConversation"; public static final String CONVERSATION = "conversationUuid"; public static final String TEXT = "text"; public static final String NICK = "nick"; - public static final String PRESENCE = "eu.siacs.conversations.presence"; public static final int REQUEST_SEND_MESSAGE = 0x0201; public static final int REQUEST_DECRYPT_PGP = 0x0202; public static final int REQUEST_ENCRYPT_MESSAGE = 0x0207; - private static final int REQUEST_ATTACH_IMAGE_DIALOG = 0x0203; - private static final int REQUEST_IMAGE_CAPTURE = 0x0204; - private static final int REQUEST_RECORD_AUDIO = 0x0205; - private static final int REQUEST_SEND_PGP_IMAGE = 0x0206; - private static final int REQUEST_ATTACH_FILE_DIALOG = 0x0208; private static final int ATTACHMENT_CHOICE_CHOOSE_IMAGE = 0x0301; private static final int ATTACHMENT_CHOICE_TAKE_PHOTO = 0x0302; private static final int ATTACHMENT_CHOICE_CHOOSE_FILE = 0x0303; + private static final int ATTACHMENT_CHOICE_RECORD_VOICE = 0x0304; private static final String STATE_OPEN_CONVERSATION = "state_open_conversation"; private static final String STATE_PANEL_OPEN = "state_panel_open"; private static final String STATE_PENDING_URI = "state_pending_uri"; @@ -81,6 +81,8 @@ public class ConversationActivity extends XmppActivity implements private Toast prepareFileToast; + private boolean mActivityPaused = false; + public List<Conversation> getConversationList() { return this.conversationList; @@ -94,10 +96,6 @@ public class ConversationActivity extends XmppActivity implements this.mSelectedConversation = conversation; } - public ListView getConversationListView() { - return this.listView; - } - public void showConversationsOverview() { if (mContentView instanceof SlidingPaneLayout) { SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; @@ -144,12 +142,12 @@ public class ConversationActivity extends XmppActivity implements protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) {mOpenConverstaion = savedInstanceState.getString( - STATE_OPEN_CONVERSATION, null); - mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true); - String pending = savedInstanceState.getString(STATE_PENDING_URI, null); - if (pending != null) { - mPendingImageUri = Uri.parse(pending); - } + STATE_OPEN_CONVERSATION, null); + mPanelOpen = savedInstanceState.getBoolean(STATE_PANEL_OPEN, true); + String pending = savedInstanceState.getString(STATE_PENDING_URI, null); + if (pending != null) { + mPendingImageUri = Uri.parse(pending); + } } setContentView(R.layout.fragment_conversations_overview); @@ -172,7 +170,7 @@ public class ConversationActivity extends XmppActivity implements @Override public void onItemClick(AdapterView<?> arg0, View clickedView, - int position, long arg3) { + int position, long arg3) { if (getSelectedConversation() != conversationList.get(position)) { setSelectedConversation(conversationList.get(position)); ConversationActivity.this.mConversationFragment.reInit(getSelectedConversation()); @@ -188,7 +186,7 @@ public class ConversationActivity extends XmppActivity implements SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; mSlidingPaneLayout.setParallaxDistance(150); mSlidingPaneLayout - .setShadowResource(R.drawable.es_slidingpane_shadow); + .setShadowResource(R.drawable.es_slidingpane_shadow); mSlidingPaneLayout.setSliderFadeColor(0); mSlidingPaneLayout.setPanelSlideListener(new PanelSlideListener() { @@ -199,7 +197,7 @@ public class ConversationActivity extends XmppActivity implements hideKeyboard(); if (xmppConnectionServiceBound) { xmppConnectionService.getNotificationService() - .setOpenConversation(null); + .setOpenConversation(null); } closeContextMenu(); } @@ -244,7 +242,7 @@ public class ConversationActivity extends XmppActivity implements if (conversation.getMode() == Conversation.MODE_SINGLE || useSubjectToIdentifyConference()) { ab.setTitle(conversation.getName()); } else { - ab.setTitle(conversation.getContactJid().toBareJid().toString()); + ab.setTitle(conversation.getJid().toBareJid().toString()); } } else { ab.setDisplayHomeAsUpEnabled(false); @@ -258,28 +256,33 @@ public class ConversationActivity extends XmppActivity implements this.updateActionBarTitle(); this.invalidateOptionsMenu(); if (xmppConnectionServiceBound) { - xmppConnectionService.getNotificationService().setOpenConversation(getSelectedConversation()); - if (!getSelectedConversation().isRead()) { - xmppConnectionService.markRead(getSelectedConversation(), true); - listView.invalidateViews(); - } + final Conversation conversation = getSelectedConversation(); + xmppConnectionService.getNotificationService().setOpenConversation(conversation); + sendReadMarkerIfNecessary(conversation); + } + } + + public void sendReadMarkerIfNecessary(final Conversation conversation) { + if (!mActivityPaused && !conversation.isRead()) { + xmppConnectionService.sendReadMarker(conversation); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.conversations, menu); - MenuItem menuSecure = menu.findItem(R.id.action_security); - MenuItem menuArchive = menu.findItem(R.id.action_archive); - MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details); - MenuItem menuContactDetails = menu - .findItem(R.id.action_contact_details); - MenuItem menuAttach = menu.findItem(R.id.action_attach_file); - MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history); - MenuItem menuAdd = menu.findItem(R.id.action_add); - MenuItem menuInviteContact = menu.findItem(R.id.action_invite); - MenuItem menuMute = menu.findItem(R.id.action_mute); - MenuItem menuUnmute = menu.findItem(R.id.action_unmute); + final MenuItem menuSecure = menu.findItem(R.id.action_security); + final MenuItem menuArchive = menu.findItem(R.id.action_archive); + final MenuItem menuMucDetails = menu.findItem(R.id.action_muc_details); + final MenuItem menuContactDetails = menu.findItem(R.id.action_contact_details); + final MenuItem menuAttach = menu.findItem(R.id.action_attach_file); + final MenuItem menuClearHistory = menu.findItem(R.id.action_clear_history); + final MenuItem menuAdd = menu.findItem(R.id.action_add); + final MenuItem menuInviteContact = menu.findItem(R.id.action_invite); + final MenuItem menuMute = menu.findItem(R.id.action_mute); + final MenuItem menuUnmute = menu.findItem(R.id.action_unmute); + final MenuItem menuBlock = menu.findItem(R.id.action_block); + final MenuItem menuUnblock = menu.findItem(R.id.action_unblock); if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) { @@ -292,19 +295,33 @@ public class ConversationActivity extends XmppActivity implements menuClearHistory.setVisible(false); menuMute.setVisible(false); menuUnmute.setVisible(false); + menuBlock.setVisible(false); + menuUnblock.setVisible(false); } else { menuAdd.setVisible(!isConversationsOverviewHideable()); if (this.getSelectedConversation() != null) { if (this.getSelectedConversation().getLatestMessage() .getEncryption() != Message.ENCRYPTION_NONE) { menuSecure.setIcon(R.drawable.ic_action_secure); - } + } if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) { menuContactDetails.setVisible(false); menuAttach.setVisible(false); + menuBlock.setVisible(false); + menuUnblock.setVisible(false); } else { menuMucDetails.setVisible(false); menuInviteContact.setTitle(R.string.conference_with); + if (this.getSelectedConversation().isBlocked()) { + menuBlock.setVisible(false); + } else { + menuUnblock.setVisible(false); + } + final Account account = this.getSelectedConversation().getAccount(); + if (!(account.isOnlineAndConnected() && account.getXmppConnection().getFeatures().blocking())) { + menuBlock.setVisible(false); + menuUnblock.setVisible(false); + } } if (this.getSelectedConversation().isMuted()) { menuMute.setVisible(false); @@ -321,33 +338,37 @@ public class ConversationActivity extends XmppActivity implements @Override public void onPresenceSelected() { - if (attachmentChoice == ATTACHMENT_CHOICE_TAKE_PHOTO) { - mPendingImageUri = xmppConnectionService.getFileBackend() - .getTakePhotoUri(); - Intent takePictureIntent = new Intent( - MediaStore.ACTION_IMAGE_CAPTURE); - takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, - mPendingImageUri); - if (takePictureIntent.resolveActivity(getPackageManager()) != null) { - startActivityForResult(takePictureIntent, - REQUEST_IMAGE_CAPTURE); + Intent intent = new Intent(); + boolean chooser = false; + switch (attachmentChoice) { + case ATTACHMENT_CHOICE_CHOOSE_IMAGE: + intent.setAction(Intent.ACTION_GET_CONTENT); + intent.setType("image/*"); + chooser = true; + break; + case ATTACHMENT_CHOICE_TAKE_PHOTO: + mPendingImageUri = xmppConnectionService.getFileBackend().getTakePhotoUri(); + intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE); + intent.putExtra(MediaStore.EXTRA_OUTPUT,mPendingImageUri); + break; + case ATTACHMENT_CHOICE_CHOOSE_FILE: + chooser = true; + intent.setType("*/*"); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setAction(Intent.ACTION_GET_CONTENT); + break; + case ATTACHMENT_CHOICE_RECORD_VOICE: + intent.setAction(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + break; + } + if (intent.resolveActivity(getPackageManager()) != null) { + if (chooser) { + startActivityForResult( + Intent.createChooser(intent,getString(R.string.perform_action_with)), + attachmentChoice); + } else { + startActivityForResult(intent, attachmentChoice); } - } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { - Intent attachFileIntent = new Intent(); - attachFileIntent.setType("image/*"); - attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); - Intent chooser = Intent.createChooser(attachFileIntent, - getString(R.string.attach_file)); - startActivityForResult(chooser, REQUEST_ATTACH_IMAGE_DIALOG); - } else if (attachmentChoice == ATTACHMENT_CHOICE_CHOOSE_FILE) { - Intent attachFileIntent = new Intent(); - //attachFileIntent.setType("file/*"); - attachFileIntent.setType("*/*"); - attachFileIntent.addCategory(Intent.CATEGORY_OPENABLE); - attachFileIntent.setAction(Intent.ACTION_GET_CONTENT); - Intent chooser = Intent.createChooser(attachFileIntent, - getString(R.string.attach_file)); - startActivityForResult(chooser, REQUEST_ATTACH_FILE_DIALOG); } } }); @@ -364,7 +385,7 @@ public class ConversationActivity extends XmppActivity implements @Override public void userInputRequried(PendingIntent pi, - Contact contact) { + Contact contact) { ConversationActivity.this.runIntent(pi, attachmentChoice); } @@ -381,18 +402,18 @@ public class ConversationActivity extends XmppActivity implements }); } else { final ConversationFragment fragment = (ConversationFragment) getFragmentManager() - .findFragmentByTag("conversation"); + .findFragmentByTag("conversation"); if (fragment != null) { fragment.showNoPGPKeyDialog(false, new OnClickListener() { @Override public void onClick(DialogInterface dialog, - int which) { + int which) { conversation - .setNextEncryption(Message.ENCRYPTION_NONE); + .setNextEncryption(Message.ENCRYPTION_NONE); xmppConnectionService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); selectPresenceToAttachFile(attachmentChoice); } }); @@ -402,7 +423,7 @@ public class ConversationActivity extends XmppActivity implements showInstallPgpDialog(); } } else if (getSelectedConversation().getNextEncryption( - forceEncryption()) == Message.ENCRYPTION_NONE) { + forceEncryption()) == Message.ENCRYPTION_NONE) { selectPresenceToAttachFile(attachmentChoice); } else { selectPresenceToAttachFile(attachmentChoice); @@ -410,7 +431,7 @@ public class ConversationActivity extends XmppActivity implements } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { if (item.getItemId() == android.R.id.home) { showConversationsOverview(); return true; @@ -455,6 +476,12 @@ public class ConversationActivity extends XmppActivity implements case R.id.action_unmute: unmuteConversation(getSelectedConversation()); break; + case R.id.action_block: + BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation()); + break; + case R.id.action_unblock: + BlockContactDialog.show(this, xmppConnectionService, getSelectedConversation()); + break; default: break; } @@ -483,7 +510,7 @@ public class ConversationActivity extends XmppActivity implements View dialogView = getLayoutInflater().inflate( R.layout.dialog_clear_history, null); final CheckBox endConversationCheckBox = (CheckBox) dialogView - .findViewById(R.id.end_conversation_checkbox); + .findViewById(R.id.end_conversation_checkbox); builder.setView(dialogView); builder.setNegativeButton(getString(R.string.cancel), null); builder.setPositiveButton(getString(R.string.delete_messages), @@ -491,10 +518,12 @@ public class ConversationActivity extends XmppActivity implements @Override public void onClick(DialogInterface dialog, int which) { - ConversationActivity.this.xmppConnectionService - .clearConversationHistory(conversation); + ConversationActivity.this.xmppConnectionService.clearConversationHistory(conversation); if (endConversationCheckBox.isChecked()) { endConversation(conversation); + } else { + updateConversationList(); + ConversationActivity.this.mConversationFragment.updateMessages(); } } }); @@ -508,28 +537,68 @@ public class ConversationActivity extends XmppActivity implements } PopupMenu attachFilePopup = new PopupMenu(this, menuAttachFile); attachFilePopup.inflate(R.menu.attachment_choices); - attachFilePopup - .setOnMenuItemClickListener(new OnMenuItemClickListener() { + if (new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION).resolveActivity(getPackageManager()) == null) { + attachFilePopup.getMenu().findItem(R.id.attach_record_voice).setVisible(false); + } + attachFilePopup.setOnMenuItemClickListener(new OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - switch (item.getItemId()) { - case R.id.attach_choose_picture: - attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); - break; - case R.id.attach_take_picture: - attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); - break; - case R.id.attach_record_voice: - attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE); - break; - } - return false; + @Override + public boolean onMenuItemClick(MenuItem item) { + switch (item.getItemId()) { + case R.id.attach_choose_picture: + attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); + break; + case R.id.attach_take_picture: + attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); + break; + case R.id.attach_choose_file: + attachFile(ATTACHMENT_CHOICE_CHOOSE_FILE); + break; + case R.id.attach_record_voice: + attachFile(ATTACHMENT_CHOICE_RECORD_VOICE); + break; } - }); + return false; + } + }); attachFilePopup.show(); } + public void verifyOtrSessionDialog(final Conversation conversation, View view) { + if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { + Toast.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show(); + return; + } + if (view == null) { + return; + } + PopupMenu popup = new PopupMenu(this, view); + popup.inflate(R.menu.verification_choices); + popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + Intent intent = new Intent(ConversationActivity.this, VerifyOTRActivity.class); + intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); + intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); + switch (menuItem.getItemId()) { + case R.id.scan_fingerprint: + intent.putExtra("mode",VerifyOTRActivity.MODE_SCAN_FINGERPRINT); + break; + case R.id.ask_question: + intent.putExtra("mode",VerifyOTRActivity.MODE_ASK_QUESTION); + break; + case R.id.manual_verification: + intent.putExtra("mode",VerifyOTRActivity.MODE_MANUAL_VERIFICATION); + break; + } + startActivity(intent); + return true; + } + }); + popup.show(); + } + protected void selectEncryptionDialog(final Conversation conversation) { View menuItemView = findViewById(R.id.action_security); if (menuItemView == null) { @@ -537,7 +606,7 @@ public class ConversationActivity extends XmppActivity implements } PopupMenu popup = new PopupMenu(this, menuItemView); final ConversationFragment fragment = (ConversationFragment) getFragmentManager() - .findFragmentByTag("conversation"); + .findFragmentByTag("conversation"); if (fragment != null) { popup.setOnMenuItemClickListener(new OnMenuItemClickListener() { @@ -557,7 +626,7 @@ public class ConversationActivity extends XmppActivity implements if (conversation.getAccount().getKeys() .has("pgp_signature")) { conversation - .setNextEncryption(Message.ENCRYPTION_PGP); + .setNextEncryption(Message.ENCRYPTION_PGP); item.setChecked(true); } else { announcePgp(conversation.getAccount(), @@ -572,7 +641,7 @@ public class ConversationActivity extends XmppActivity implements break; } xmppConnectionService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); fragment.updateChatMsgHint(); return true; } @@ -597,11 +666,11 @@ public class ConversationActivity extends XmppActivity implements break; case Message.ENCRYPTION_PGP: popup.getMenu().findItem(R.id.encryption_choice_pgp) - .setChecked(true); + .setChecked(true); break; default: popup.getMenu().findItem(R.id.encryption_choice_none) - .setChecked(true); + .setChecked(true); break; } popup.show(); @@ -617,17 +686,17 @@ public class ConversationActivity extends XmppActivity implements new OnClickListener() { @Override - public void onClick(DialogInterface dialog, int which) { - long till; + public void onClick(final DialogInterface dialog, final int which) { + final long till; if (durations[which] == -1) { till = Long.MAX_VALUE; } else { till = SystemClock.elapsedRealtime() - + (durations[which] * 1000); + + (durations[which] * 1000); } conversation.setMutedTill(till); ConversationActivity.this.xmppConnectionService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); updateConversationList(); ConversationActivity.this.mConversationFragment.updateMessages(); invalidateOptionsMenu(); @@ -676,6 +745,28 @@ public class ConversationActivity extends XmppActivity implements } @Override + public void onPause() { + super.onPause(); + this.mActivityPaused = true; + if (this.xmppConnectionServiceBound) { + this.xmppConnectionService.getNotificationService().setIsInForeground(false); + } + } + + @Override + public void onResume() { + super.onResume(); + int theme = findTheme(); + if (this.mTheme != theme) { + recreate(); + } + this.mActivityPaused = false; + if (this.xmppConnectionServiceBound) { + this.xmppConnectionService.getNotificationService().setIsInForeground(true); + } + } + + @Override public void onSaveInstanceState(final Bundle savedInstanceState) { Conversation conversation = getSelectedConversation(); if (conversation != null) { @@ -692,6 +783,7 @@ public class ConversationActivity extends XmppActivity implements @Override void onBackendConnected() { + this.xmppConnectionService.getNotificationService().setIsInForeground(true); updateConversationList(); if (xmppConnectionService.getAccounts().size() == 0) { startActivity(new Intent(this, EditAccountActivity.class)); @@ -752,11 +844,11 @@ public class ConversationActivity extends XmppActivity implements } private void selectConversationByUuid(String uuid) { - for (Conversation aConversationList : conversationList) { - if (aConversationList.getUuid().equals(uuid)) { - setSelectedConversation(aConversationList); - } - } + for (Conversation aConversationList : conversationList) { + if (aConversationList.getUuid().equals(uuid)) { + setSelectedConversation(aConversationList); + } + } } @Override @@ -767,50 +859,35 @@ public class ConversationActivity extends XmppActivity implements @Override protected void onActivityResult(int requestCode, int resultCode, - final Intent data) { + final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode == RESULT_OK) { if (requestCode == REQUEST_DECRYPT_PGP) { mConversationFragment.hideSnackbar(); mConversationFragment.updateMessages(); - } else if (requestCode == REQUEST_ATTACH_IMAGE_DIALOG) { + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { mPendingImageUri = data.getData(); if (xmppConnectionServiceBound) { - attachImageToConversation(getSelectedConversation(), - mPendingImageUri); + attachImageToConversation(getSelectedConversation(),mPendingImageUri); mPendingImageUri = null; } - } else if (requestCode == REQUEST_ATTACH_FILE_DIALOG) { + } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_FILE || requestCode == ATTACHMENT_CHOICE_RECORD_VOICE) { mPendingFileUri = data.getData(); if (xmppConnectionServiceBound) { - attachFileToConversation(getSelectedConversation(), - mPendingFileUri); + attachFileToConversation(getSelectedConversation(),mPendingFileUri); mPendingFileUri = null; } - } else if (requestCode == REQUEST_SEND_PGP_IMAGE) { - - } else if (requestCode == ATTACHMENT_CHOICE_CHOOSE_IMAGE) { - attachFile(ATTACHMENT_CHOICE_CHOOSE_IMAGE); - } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { - attachFile(ATTACHMENT_CHOICE_TAKE_PHOTO); - } else if (requestCode == REQUEST_ANNOUNCE_PGP) { - announcePgp(getSelectedConversation().getAccount(), - getSelectedConversation()); - } else if (requestCode == REQUEST_ENCRYPT_MESSAGE) { - // encryptTextMessage(); - } else if (requestCode == REQUEST_IMAGE_CAPTURE && mPendingImageUri != null) { + } else if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO && mPendingImageUri != null) { if (xmppConnectionServiceBound) { - attachImageToConversation(getSelectedConversation(), - mPendingImageUri); + attachImageToConversation(getSelectedConversation(),mPendingImageUri); mPendingImageUri = null; } - Intent intent = new Intent( - Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); intent.setData(mPendingImageUri); sendBroadcast(intent); } } else { - if (requestCode == REQUEST_IMAGE_CAPTURE) { + if (requestCode == ATTACHMENT_CHOICE_TAKE_PHOTO) { mPendingImageUri = null; } } @@ -848,10 +925,8 @@ public class ConversationActivity extends XmppActivity implements @Override public void userInputRequried(PendingIntent pi, - Message object) { + Message object) { hidePrepareFileToast(); - ConversationActivity.this.runIntent(pi, - ConversationActivity.REQUEST_SEND_PGP_IMAGE); } @Override @@ -881,7 +956,7 @@ public class ConversationActivity extends XmppActivity implements public void updateConversationList() { xmppConnectionService - .populateWithOrderedConversations(conversationList); + .populateWithOrderedConversations(conversationList); listAdapter.notifyDataSetChanged(); } @@ -899,7 +974,7 @@ public class ConversationActivity extends XmppActivity implements @Override public void userInputRequried(PendingIntent pi, - Message message) { + Message message) { ConversationActivity.this.runIntent(pi, ConversationActivity.REQUEST_SEND_MESSAGE); } @@ -951,7 +1026,7 @@ public class ConversationActivity extends XmppActivity implements updateConversationList(); if (conversationList.size() == 0) { startActivity(new Intent(getApplicationContext(), - StartConversationActivity.class)); + StartConversationActivity.class)); finish(); } ConversationActivity.this.mConversationFragment.updateMessages(); @@ -964,12 +1039,31 @@ public class ConversationActivity extends XmppActivity implements public void onRosterUpdate() { runOnUiThread(new Runnable() { - @Override - public void run() { - updateConversationList(); - ConversationActivity.this.mConversationFragment.updateMessages(); - updateActionBarTitle(); - } - }); + @Override + public void run() { + updateConversationList(); + ConversationActivity.this.mConversationFragment.updateMessages(); + updateActionBarTitle(); + } + }); + } + + @Override + public void OnUpdateBlocklist(Status status) { + invalidateOptionsMenu(); + runOnUiThread(new Runnable() { + @Override + public void run() { + ConversationActivity.this.mConversationFragment.updateMessages(); + } + }); + } + + public void unblockConversation(final Blockable conversation) { + xmppConnectionService.sendUnblockRequest(conversation); + } + + public void blockConversation(final Blockable conversation) { + xmppConnectionService.sendBlockRequest(conversation); } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0edc6b6f..93a4cc7e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -103,7 +103,9 @@ public class ConversationFragment extends Fragment { private RelativeLayout snackbar; private TextView snackbarMessage; private TextView snackbarAction; - private boolean messagesLoaded = false; + private boolean messagesLoaded = true; + private Toast messageLoaderToast; + private OnScrollListener mOnScrollListener = new OnScrollListener() { @Override @@ -114,20 +116,69 @@ public class ConversationFragment extends Fragment { @Override public void onScroll(AbsListView view, int firstVisibleItem, - int visibleItemCount, int totalItemCount) { + int visibleItemCount, int totalItemCount) { synchronized (ConversationFragment.this.messageList) { - if (firstVisibleItem == 0 && messagesLoaded) { + if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) { long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent(); messagesLoaded = false; - int size = activity.xmppConnectionService.loadMoreMessages(conversation, timestamp); - ConversationFragment.this.messageList.clear(); - ConversationFragment.this.messageList.addAll(conversation.getMessages()); - updateStatusMessages(); - messageListAdapter.notifyDataSetChanged(); - if (size != 0) { - messagesLoaded = true; - } - messagesView.setSelectionFromTop(size + 1, 0); + activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { + @Override + public void onMoreMessagesLoaded(final int count, Conversation conversation) { + if (ConversationFragment.this.conversation != conversation) { + return; + } + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + final int oldPosition = messagesView.getFirstVisiblePosition(); + View v = messagesView.getChildAt(0); + final int pxOffset = (v == null) ? 0 : v.getTop(); + ConversationFragment.this.conversation.populateWithMessages(ConversationFragment.this.messageList); + updateStatusMessages(); + messageListAdapter.notifyDataSetChanged(); + if (count != 0) { + final int newPosition = oldPosition + count; + int offset = 0; + try { + Message tmpMessage = messageList.get(newPosition); + + while(tmpMessage.wasMergedIntoPrevious()) { + offset++; + tmpMessage = tmpMessage.prev(); + } + } catch (final IndexOutOfBoundsException ignored) { + + } + messagesView.setSelectionFromTop(newPosition - offset, pxOffset); + messagesLoaded = true; + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + } + } + }); + } + + @Override + public void informUser(final int resId) { + + activity.runOnUiThread(new Runnable() { + @Override + public void run() { + if (messageLoaderToast != null) { + messageLoaderToast.cancel(); + } + if (ConversationFragment.this.conversation != conversation) { + return; + } + messageLoaderToast = Toast.makeText(activity,resId,Toast.LENGTH_LONG); + messageLoaderToast.show(); + } + }); + + } + }); + } } } @@ -153,13 +204,7 @@ public class ConversationFragment extends Fragment { @Override public void onClick(View v) { - if (conversation.getOtrFingerprint() != null) { - Intent intent = new Intent(getActivity(), VerifyOTRActivity.class); - intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); - intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); - intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); - startActivity(intent); - } + activity.verifyOtrSessionDialog(conversation,v); } }; private ConcurrentLinkedQueue<Message> mEncryptedMessages = new ConcurrentLinkedQueue<>(); @@ -170,7 +215,7 @@ public class ConversationFragment extends Fragment { public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { if (actionId == EditorInfo.IME_ACTION_SEND) { InputMethodManager imm = (InputMethodManager) v.getContext() - .getSystemService(Context.INPUT_METHOD_SERVICE); + .getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(v.getWindowToken(), 0); sendMessage(); return true; @@ -213,7 +258,7 @@ public class ConversationFragment extends Fragment { } Message message = new Message(conversation, mEditMessage.getText() .toString(), conversation.getNextEncryption(activity - .forceEncryption())); + .forceEncryption())); if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getNextCounterpart() != null) { message.setCounterpart(conversation.getNextCounterpart()); @@ -234,13 +279,13 @@ public class ConversationFragment extends Fragment { if (conversation.getMode() == Conversation.MODE_MULTI && conversation.getNextCounterpart() != null) { this.mEditMessage.setHint(getString( - R.string.send_private_message_to, - conversation.getNextCounterpart().getResourcepart())); + R.string.send_private_message_to, + conversation.getNextCounterpart().getResourcepart())); } else { switch (conversation.getNextEncryption(activity.forceEncryption())) { case Message.ENCRYPTION_NONE: mEditMessage - .setHint(getString(R.string.send_plain_text_message)); + .setHint(getString(R.string.send_plain_text_message)); break; case Message.ENCRYPTION_OTR: mEditMessage.setHint(getString(R.string.send_otr_message)); @@ -256,7 +301,7 @@ public class ConversationFragment extends Fragment { @Override public View onCreateView(final LayoutInflater inflater, - ViewGroup container, Bundle savedInstanceState) { + ViewGroup container, Bundle savedInstanceState) { final View view = inflater.inflate(R.layout.fragment_conversation, container, false); mEditMessage = (EditMessage) view.findViewById(R.id.textinput); @@ -289,49 +334,51 @@ public class ConversationFragment extends Fragment { messageListAdapter = new MessageAdapter((ConversationActivity) getActivity(), this.messageList); messageListAdapter.setOnContactPictureClicked(new OnContactPictureClicked() { - @Override - public void onContactPictureClicked(Message message) { - if (message.getStatus() <= Message.STATUS_RECEIVED) { - if (message.getConversation().getMode() == Conversation.MODE_MULTI) { - if (message.getCounterpart() != null) { - if (!message.getCounterpart().isBareJid()) { - highlightInConference(message.getCounterpart().getResourcepart()); - } else { - highlightInConference(message.getCounterpart().toString()); - } - } + @Override + public void onContactPictureClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getCounterpart() != null) { + if (!message.getCounterpart().isBareJid()) { + highlightInConference(message.getCounterpart().getResourcepart()); } else { - Contact contact = message.getConversation() - .getContact(); - if (contact.showInRoster()) { - activity.switchToContactDetails(contact); - } else { - activity.showAddToRosterDialog(message - .getConversation()); - } + highlightInConference(message.getCounterpart().toString()); } + } + } else { + Contact contact = message.getConversation() + .getContact(); + if (contact.showInRoster()) { + activity.switchToContactDetails(contact); } else { - Account account = message.getConversation().getAccount(); - Intent intent = new Intent(activity, EditAccountActivity.class); - intent.putExtra("jid", account.getJid().toBareJid().toString()); - startActivity(intent); + activity.showAddToRosterDialog(message + .getConversation()); } } - }); + } else { + Account account = message.getConversation().getAccount(); + Intent intent = new Intent(activity, EditAccountActivity.class); + intent.putExtra("jid", account.getJid().toBareJid().toString()); + startActivity(intent); + } + } + }); messageListAdapter - .setOnContactPictureLongClicked(new OnContactPictureLongClicked() { + .setOnContactPictureLongClicked(new OnContactPictureLongClicked() { - @Override - public void onContactPictureLongClicked(Message message) { - if (message.getStatus() <= Message.STATUS_RECEIVED) { - if (message.getConversation().getMode() == Conversation.MODE_MULTI) { - if (message.getCounterpart() != null) { - privateMessageWith(message.getCounterpart()); - } + @Override + public void onContactPictureLongClicked(Message message) { + if (message.getStatus() <= Message.STATUS_RECEIVED) { + if (message.getConversation().getMode() == Conversation.MODE_MULTI) { + if (message.getCounterpart() != null) { + privateMessageWith(message.getCounterpart()); } } + } else { + activity.showQrCode(); } - }); + } + }); messagesView.setAdapter(messageListAdapter); registerForContextMenu(messagesView); @@ -341,7 +388,7 @@ public class ConversationFragment extends Fragment { @Override public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { + ContextMenuInfo menuInfo) { synchronized (this.messageList) { super.onCreateContextMenu(menu, v, menuInfo); AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; @@ -351,7 +398,8 @@ public class ConversationFragment extends Fragment { } private void populateContextMenu(ContextMenu menu) { - if (this.selectedMessage.getType() != Message.TYPE_STATUS) { + final Message m = this.selectedMessage; + 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); @@ -360,29 +408,26 @@ public class ConversationFragment extends Fragment { MenuItem copyUrl = menu.findItem(R.id.copy_url); MenuItem downloadImage = menu.findItem(R.id.download_image); MenuItem cancelTransmission = menu.findItem(R.id.cancel_transmission); - if (this.selectedMessage.getType() != Message.TYPE_TEXT - || this.selectedMessage.getDownloadable() != null) { + if (m.getType() != Message.TYPE_TEXT || m.getDownloadable() != null) { copyText.setVisible(false); } - if (this.selectedMessage.getType() != Message.TYPE_IMAGE - || this.selectedMessage.getDownloadable() != null) { + if (m.getType() != Message.TYPE_IMAGE || m.getDownloadable() != null) { shareImage.setVisible(false); } - if (this.selectedMessage.getStatus() != Message.STATUS_SEND_FAILED) { + if (m.getStatus() != Message.STATUS_SEND_FAILED) { sendAgain.setVisible(false); } - if ((this.selectedMessage.getType() != Message.TYPE_IMAGE && this.selectedMessage - .getDownloadable() == null) - || this.selectedMessage.getImageParams().url == null) { + if ((m.getType() != Message.TYPE_IMAGE && m.getDownloadable() == null) + || m.getImageParams().url == null) { copyUrl.setVisible(false); } - if (this.selectedMessage.getType() != Message.TYPE_TEXT - || this.selectedMessage.getDownloadable() != null - || !this.selectedMessage.bodyContainsDownloadable()) { + if (m.getType() != Message.TYPE_TEXT + || m.getDownloadable() != null + || !m.bodyContainsDownloadable()) { downloadImage.setVisible(false); } - if (this.selectedMessage.getDownloadable() == null - || this.selectedMessage.getDownloadable() instanceof DownloadablePlaceholder) { + if (!((m.getDownloadable() != null && !(m.getDownloadable() instanceof DownloadablePlaceholder)) + || (m.isFileOrImage() && m.getStatus() == Message.STATUS_WAITING))) { cancelTransmission.setVisible(false); } } @@ -419,16 +464,16 @@ public class ConversationFragment extends Fragment { shareIntent.setAction(Intent.ACTION_SEND); shareIntent.putExtra(Intent.EXTRA_STREAM, activity.xmppConnectionService.getFileBackend() - .getJingleFileUri(message)); + .getJingleFileUri(message)); shareIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); shareIntent.setType("image/webp"); activity.startActivity(Intent.createChooser(shareIntent, - getText(R.string.share_with))); + getText(R.string.share_with))); } private void copyText(Message message) { if (activity.copyTextToClipboard(message.getMergedBody(), - R.string.message_text)) { + R.string.message_text)) { Toast.makeText(activity, R.string.message_copied_to_clipboard, Toast.LENGTH_SHORT).show(); } @@ -448,21 +493,23 @@ public class ConversationFragment extends Fragment { private void copyUrl(Message message) { if (activity.copyTextToClipboard( - message.getImageParams().url.toString(), R.string.image_url)) { + message.getImageParams().url.toString(), R.string.image_url)) { Toast.makeText(activity, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show(); - } + } } private void downloadImage(Message message) { activity.xmppConnectionService.getHttpConnectionManager() - .createNewConnection(message); + .createNewConnection(message); } private void cancelTransmission(Message message) { Downloadable downloadable = message.getDownloadable(); if (downloadable!=null) { downloadable.cancel(); + } else { + activity.xmppConnectionService.markMessage(message,Message.STATUS_SEND_FAILED); } } @@ -478,9 +525,9 @@ public class ConversationFragment extends Fragment { mEditMessage.getText().insert(0, nick + ": "); } else { if (mEditMessage.getText().charAt( - mEditMessage.getSelectionStart() - 1) != ' ') { + mEditMessage.getSelectionStart() - 1) != ' ') { nick = " " + nick; - } + } mEditMessage.getText().insert(mEditMessage.getSelectionStart(), nick + " "); } @@ -501,6 +548,7 @@ public class ConversationFragment extends Fragment { } if (this.conversation != null) { this.conversation.setNextMessage(mEditMessage.getText().toString()); + this.conversation.trim(); } this.activity = (ConversationActivity) getActivity(); this.conversation = conversation; @@ -511,8 +559,13 @@ public class ConversationFragment extends Fragment { } this.mEditMessage.setText(""); this.mEditMessage.append(this.conversation.getNextMessage()); - this.messagesView.invalidate(); + this.messagesView.invalidateViews(); updateMessages(); + this.messagesLoaded = true; + int size = this.messageList.size(); + if (size > 0) { + messagesView.setSelection(size - 1); + } } public void updateMessages() { @@ -524,12 +577,30 @@ public class ConversationFragment extends Fragment { final ConversationActivity activity = (ConversationActivity) getActivity(); if (this.conversation != null) { final Contact contact = this.conversation.getContact(); - if (this.conversation.isMuted()) { + if (this.conversation.isBlocked()) { + showSnackbar(R.string.contact_blocked, R.string.unblock, + new OnClickListener() { + @Override + public void onClick(final View v) { + v.post(new Runnable() { + @Override + public void run() { + v.setVisibility(View.INVISIBLE); + } + }); + if (conversation.isDomainBlocked()) { + BlockContactDialog.show(getActivity(), ((ConversationActivity) getActivity()).xmppConnectionService, conversation); + } else { + ((ConversationActivity) getActivity()).unblockConversation(conversation); + } + } + }); + } else if (this.conversation.isMuted()) { showSnackbar(R.string.notifications_disabled, R.string.enable, new OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { activity.unmuteConversation(conversation); } }); @@ -542,7 +613,7 @@ public class ConversationFragment extends Fragment { @Override public void onClick(View v) { activity.xmppConnectionService - .createContact(contact); + .createContact(contact); activity.switchToContactDetails(contact); } }); @@ -579,31 +650,24 @@ public class ConversationFragment extends Fragment { default: break; } - } - this.messageList.clear(); - if (this.conversation.getMessages().size() == 0) { - messagesLoaded = false; - } else { - this.messageList.addAll(this.conversation.getMessages()); - messagesLoaded = true; - for (Message message : this.messageList) { - if (message.getEncryption() == Message.ENCRYPTION_PGP - && (message.getStatus() == Message.STATUS_RECEIVED || message + } + conversation.populateWithMessages(ConversationFragment.this.messageList); + for (Message message : this.messageList) { + if (message.getEncryption() == Message.ENCRYPTION_PGP + && (message.getStatus() == Message.STATUS_RECEIVED || message .getStatus() >= Message.STATUS_SEND) - && message.getDownloadable() == null) { - if (!mEncryptedMessages.contains(message)) { - mEncryptedMessages.add(message); - } + && message.getDownloadable() == null) { + if (!mEncryptedMessages.contains(message)) { + mEncryptedMessages.add(message); } - } - decryptNext(); - updateStatusMessages(); + } } + decryptNext(); + updateStatusMessages(); this.messageListAdapter.notifyDataSetChanged(); updateChatMsgHint(); if (!activity.isConversationsOverviewVisable() || !activity.isConversationsOverviewHideable()) { - activity.xmppConnectionService.markRead(conversation, true); - activity.updateConversationList(); + activity.sendReadMarkerIfNecessary(conversation); } this.updateSendButton(); } @@ -667,44 +731,44 @@ public class ConversationFragment extends Fragment { switch (c.getContact().getMostAvailableStatus()) { case Presences.CHAT: this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_online); + .setImageResource(R.drawable.ic_action_send_now_online); break; case Presences.ONLINE: this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_online); + .setImageResource(R.drawable.ic_action_send_now_online); break; case Presences.AWAY: this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_away); + .setImageResource(R.drawable.ic_action_send_now_away); break; case Presences.XA: this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_away); + .setImageResource(R.drawable.ic_action_send_now_away); break; case Presences.DND: this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_dnd); + .setImageResource(R.drawable.ic_action_send_now_dnd); break; default: this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_offline); + .setImageResource(R.drawable.ic_action_send_now_offline); break; } } else if (c.getMode() == Conversation.MODE_MULTI) { if (c.getMucOptions().online()) { this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_online); + .setImageResource(R.drawable.ic_action_send_now_online); } else { this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_offline); + .setImageResource(R.drawable.ic_action_send_now_offline); } } else { this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_offline); + .setImageResource(R.drawable.ic_action_send_now_offline); } } else { this.mSendButton - .setImageResource(R.drawable.ic_action_send_now_offline); + .setImageResource(R.drawable.ic_action_send_now_offline); } } @@ -716,8 +780,7 @@ public class ConversationFragment extends Fragment { return; } else { if (this.messageList.get(i).getStatus() == Message.STATUS_SEND_DISPLAYED) { - this.messageList.add(i + 1, - Message.createStatusMessage(conversation)); + this.messageList.add(i + 1,Message.createStatusMessage(conversation)); return; } } @@ -728,19 +791,30 @@ public class ConversationFragment extends Fragment { protected void makeFingerprintWarning() { if (conversation.smpRequested()) { - showSnackbar(R.string.smp_requested, R.string.verify, clickToVerify); + showSnackbar(R.string.smp_requested, R.string.verify, new OnClickListener() { + @Override + public void onClick(View view) { + Intent intent = new Intent(activity, VerifyOTRActivity.class); + intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); + intent.putExtra("contact", conversation.getContact().getJid().toBareJid().toString()); + intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString()); + intent.putExtra("mode",VerifyOTRActivity.MODE_ANSWER_QUESTION); + startActivity(intent); + } + }); } else if (conversation.hasValidOtrSession() && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!conversation.isOtrFingerprintVerified())) { showSnackbar(R.string.unknown_otr_fingerprint, R.string.verify, clickToVerify); - } + } } - protected void showSnackbar(int message, int action, - OnClickListener clickListener) { + protected void showSnackbar(final int message, final int action, + final OnClickListener clickListener) { snackbar.setVisibility(View.VISIBLE); snackbar.setOnClickListener(null); snackbarMessage.setText(message); snackbarMessage.setOnClickListener(null); + snackbarAction.setVisibility(View.VISIBLE); snackbarAction.setText(action); snackbarAction.setOnClickListener(clickListener); } @@ -767,7 +841,7 @@ public class ConversationFragment extends Fragment { @Override public void userInputRequried(PendingIntent pi, - Contact contact) { + Contact contact) { activity.runIntent( pi, ConversationActivity.REQUEST_ENCRYPT_MESSAGE); @@ -791,11 +865,11 @@ public class ConversationFragment extends Fragment { @Override public void onClick(DialogInterface dialog, - int which) { + int which) { conversation - .setNextEncryption(Message.ENCRYPTION_NONE); + .setNextEncryption(Message.ENCRYPTION_NONE); xmppService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); message.setEncryption(Message.ENCRYPTION_NONE); xmppService.sendMessage(message); messageSent(); @@ -806,9 +880,9 @@ public class ConversationFragment extends Fragment { if (conversation.getMucOptions().pgpKeysInUse()) { if (!conversation.getMucOptions().everybodyHasKeys()) { Toast warning = Toast - .makeText(getActivity(), - R.string.missing_public_keys, - Toast.LENGTH_LONG); + .makeText(getActivity(), + R.string.missing_public_keys, + Toast.LENGTH_LONG); warning.setGravity(Gravity.CENTER_VERTICAL, 0, 0); warning.show(); } @@ -820,12 +894,12 @@ public class ConversationFragment extends Fragment { @Override public void onClick(DialogInterface dialog, - int which) { + int which) { conversation - .setNextEncryption(Message.ENCRYPTION_NONE); + .setNextEncryption(Message.ENCRYPTION_NONE); message.setEncryption(Message.ENCRYPTION_NONE); xmppService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); xmppService.sendMessage(message); messageSent(); } @@ -838,7 +912,7 @@ public class ConversationFragment extends Fragment { } public void showNoPGPKeyDialog(boolean plural, - DialogInterface.OnClickListener listener) { + DialogInterface.OnClickListener listener) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); builder.setIconAttribute(android.R.attr.alertDialogIcon); if (plural) { diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 8fad66cf..ea45b75e 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -19,6 +19,7 @@ import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.RelativeLayout; +import android.widget.TableLayout; import android.widget.TextView; import android.widget.Toast; @@ -28,13 +29,12 @@ import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.UIHelper; -import eu.siacs.conversations.utils.Validator; import eu.siacs.conversations.xmpp.XmppConnection.Features; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; -public class EditAccountActivity extends XmppActivity implements OnAccountUpdate { +public class EditAccountActivity extends XmppActivity implements OnAccountUpdate{ private AutoCompleteTextView mAccountJid; private EditText mPassword; @@ -42,10 +42,15 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private CheckBox mRegisterNew; private Button mCancelButton; private Button mSaveButton; + private TableLayout mMoreTable; private LinearLayout mStats; private TextView mServerInfoSm; + private TextView mServerInfoRosterVersion; private TextView mServerInfoCarbons; + private TextView mServerInfoMam; + private TextView mServerInfoCSI; + private TextView mServerInfoBlocking; private TextView mServerInfoPep; private TextView mSessionEst; private TextView mOtrFingerprint; @@ -58,22 +63,16 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate private boolean mFetchingAvatar = false; - private OnClickListener mSaveButtonClickListener = new OnClickListener() { + private final OnClickListener mSaveButtonClickListener = new OnClickListener() { @Override - public void onClick(View v) { - if (mAccount != null - && mAccount.getStatus() == Account.State.DISABLED) { + public void onClick(final View v) { + if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED) { mAccount.setOption(Account.OPTION_DISABLED, false); xmppConnectionService.updateAccount(mAccount); return; } - if (!Validator.isValidJid(mAccountJid.getText().toString())) { - mAccountJid.setError(getString(R.string.invalid_jid)); - mAccountJid.requestFocus(); - return; - } - boolean registerNewAccount = mRegisterNew.isChecked(); + final boolean registerNewAccount = mRegisterNew.isChecked(); final Jid jid; try { jid = Jid.fromString(mAccountJid.getText().toString()); @@ -82,37 +81,41 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate mAccountJid.requestFocus(); return; } - String password = mPassword.getText().toString(); - String passwordConfirm = mPasswordConfirm.getText().toString(); + if (jid.isDomainJid()) { + mAccountJid.setError(getString(R.string.invalid_jid)); + mAccountJid.requestFocus(); + return; + } + final String password = mPassword.getText().toString(); + final String passwordConfirm = mPasswordConfirm.getText().toString(); if (registerNewAccount) { if (!password.equals(passwordConfirm)) { - mPasswordConfirm - .setError(getString(R.string.passwords_do_not_match)); + mPasswordConfirm.setError(getString(R.string.passwords_do_not_match)); mPasswordConfirm.requestFocus(); return; } } if (mAccount != null) { + try { + mAccount.setUsername(jid.hasLocalpart() ? jid.getLocalpart() : ""); + mAccount.setServer(jid.getDomainpart()); + } catch (final InvalidJidException ignored) { + return; + } mAccount.setPassword(password); - try { - mAccount.setUsername(jid.hasLocalpart() ? jid.getLocalpart() : ""); - mAccount.setServer(jid.getDomainpart()); - } catch (final InvalidJidException ignored) { - } mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); xmppConnectionService.updateAccount(mAccount); } else { - try { - if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) { - mAccountJid - .setError(getString(R.string.account_already_exists)); - mAccountJid.requestFocus(); - return; - } - } catch (InvalidJidException e) { - return; - } - mAccount = new Account(jid.toBareJid(), password); + try { + if (xmppConnectionService.findAccountByJid(Jid.fromString(mAccountJid.getText().toString())) != null) { + mAccountJid.setError(getString(R.string.account_already_exists)); + mAccountJid.requestFocus(); + return; + } + } catch (final InvalidJidException e) { + return; + } + mAccount = new Account(jid.toBareJid(), password); mAccount.setOption(Account.OPTION_USETLS, true); mAccount.setOption(Account.OPTION_USECOMPRESSION, true); mAccount.setOption(Account.OPTION_REGISTER, registerNewAccount); @@ -127,82 +130,81 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } }; - private OnClickListener mCancelButtonClickListener = new OnClickListener() { + private final OnClickListener mCancelButtonClickListener = new OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { finish(); } }; - @Override - public void onAccountUpdate() { - runOnUiThread(new Runnable() { - - @Override - public void run() { - if (mAccount != null - && mAccount.getStatus() != Account.State.ONLINE - && mFetchingAvatar) { - startActivity(new Intent(getApplicationContext(), + @Override + public void onAccountUpdate() { + runOnUiThread(new Runnable() { + + @Override + public void run() { + invalidateOptionsMenu(); + if (mAccount != null + && mAccount.getStatus() != Account.State.ONLINE + && mFetchingAvatar) { + startActivity(new Intent(getApplicationContext(), ManageAccountActivity.class)); - finish(); - } else if (jidToEdit == null && mAccount != null - && mAccount.getStatus() == Account.State.ONLINE) { - if (!mFetchingAvatar) { - mFetchingAvatar = true; - xmppConnectionService.checkForAvatar(mAccount, - mAvatarFetchCallback); - } - } else { - updateSaveButton(); - } - if (mAccount != null) { - updateAccountInformation(); + finish(); + } else if (jidToEdit == null && mAccount != null + && mAccount.getStatus() == Account.State.ONLINE) { + if (!mFetchingAvatar) { + mFetchingAvatar = true; + xmppConnectionService.checkForAvatar(mAccount, + mAvatarFetchCallback); } + } else { + updateSaveButton(); } - }); - } - private UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() { + if (mAccount != null) { + updateAccountInformation(); + } + } + }); + } + private final UiCallback<Avatar> mAvatarFetchCallback = new UiCallback<Avatar>() { @Override - public void userInputRequried(PendingIntent pi, Avatar avatar) { + public void userInputRequried(final PendingIntent pi, final Avatar avatar) { finishInitialSetup(avatar); } @Override - public void success(Avatar avatar) { + public void success(final Avatar avatar) { finishInitialSetup(avatar); } @Override - public void error(int errorCode, Avatar avatar) { + public void error(final int errorCode, final Avatar avatar) { finishInitialSetup(avatar); } }; - private TextWatcher mTextWatcher = new TextWatcher() { + private final TextWatcher mTextWatcher = new TextWatcher() { @Override - public void onTextChanged(CharSequence s, int start, int before, - int count) { + public void onTextChanged(final CharSequence s, final int start, final int before, final int count) { updateSaveButton(); } @Override - public void beforeTextChanged(CharSequence s, int start, int count, - int after) { - + public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) { } @Override - public void afterTextChanged(Editable s) { + public void afterTextChanged(final Editable s) { } }; - private OnClickListener mAvatarClickListener = new OnClickListener() { + + private final OnClickListener mAvatarClickListener = new OnClickListener() { @Override - public void onClick(View view) { - if (mAccount!=null) { - Intent intent = new Intent(getApplicationContext(), + public void onClick(final View view) { + if (mAccount != null) { + final Intent intent = new Intent(getApplicationContext(), PublishProfilePictureActivity.class); intent.putExtra("account", mAccount.getJid().toBareJid().toString()); startActivity(intent); @@ -215,7 +217,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate @Override public void run() { - Intent intent; + final Intent intent; if (avatar != null) { intent = new Intent(getApplicationContext(), StartConversationActivity.class); @@ -232,13 +234,11 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } protected void updateSaveButton() { - if (mAccount != null - && mAccount.getStatus() == Account.State.CONNECTING) { + if (mAccount != null && mAccount.getStatus() == Account.State.CONNECTING) { this.mSaveButton.setEnabled(false); this.mSaveButton.setTextColor(getSecondaryTextColor()); this.mSaveButton.setText(R.string.account_status_connecting); - } else if (mAccount != null - && mAccount.getStatus() == Account.State.DISABLED) { + } else if (mAccount != null && mAccount.getStatus() == Account.State.DISABLED) { this.mSaveButton.setEnabled(true); this.mSaveButton.setTextColor(getPrimaryTextColor()); this.mSaveButton.setText(R.string.enable); @@ -246,8 +246,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mSaveButton.setEnabled(true); this.mSaveButton.setTextColor(getPrimaryTextColor()); if (jidToEdit != null) { - if (mAccount != null - && mAccount.getStatus() == Account.State.ONLINE) { + if (mAccount != null && mAccount.isOnlineAndConnected()) { this.mSaveButton.setText(R.string.save); if (!accountInfoEdited()) { this.mSaveButton.setEnabled(false); @@ -263,10 +262,10 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } protected boolean accountInfoEdited() { - return (!this.mAccount.getJid().toBareJid().equals( - this.mAccountJid.getText().toString())) - || (!this.mAccount.getPassword().equals( - this.mPassword.getText().toString())); + return (!this.mAccount.getJid().toBareJid().toString().equals( + this.mAccountJid.getText().toString())) + || (!this.mAccount.getPassword().equals( + this.mPassword.getText().toString())); } @Override @@ -279,7 +278,7 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate } @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit_account); this.mAccountJid = (AutoCompleteTextView) findViewById(R.id.account_jid); @@ -292,7 +291,11 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mRegisterNew = (CheckBox) findViewById(R.id.account_register_new); this.mStats = (LinearLayout) findViewById(R.id.stats); this.mSessionEst = (TextView) findViewById(R.id.session_est); + this.mServerInfoRosterVersion = (TextView) findViewById(R.id.server_info_roster_version); this.mServerInfoCarbons = (TextView) findViewById(R.id.server_info_carbons); + this.mServerInfoMam = (TextView) findViewById(R.id.server_info_mam); + this.mServerInfoCSI = (TextView) findViewById(R.id.server_info_csi); + this.mServerInfoBlocking = (TextView) findViewById(R.id.server_info_blocking); this.mServerInfoSm = (TextView) findViewById(R.id.server_info_sm); this.mServerInfoPep = (TextView) findViewById(R.id.server_info_pep); this.mOtrFingerprint = (TextView) findViewById(R.id.otr_fingerprint); @@ -302,29 +305,41 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mCancelButton = (Button) findViewById(R.id.cancel_button); this.mSaveButton.setOnClickListener(this.mSaveButtonClickListener); this.mCancelButton.setOnClickListener(this.mCancelButtonClickListener); - this.mRegisterNew - .setOnCheckedChangeListener(new OnCheckedChangeListener() { - - @Override - public void onCheckedChanged(CompoundButton buttonView, - boolean isChecked) { - if (isChecked) { - mPasswordConfirm.setVisibility(View.VISIBLE); - } else { - mPasswordConfirm.setVisibility(View.GONE); - } - updateSaveButton(); - } - }); + this.mMoreTable = (TableLayout) findViewById(R.id.server_info_more); + final OnCheckedChangeListener OnCheckedShowConfirmPassword = new OnCheckedChangeListener() { + @Override + public void onCheckedChanged(final CompoundButton buttonView, + final boolean isChecked) { + if (isChecked) { + mPasswordConfirm.setVisibility(View.VISIBLE); + } else { + mPasswordConfirm.setVisibility(View.GONE); + } + updateSaveButton(); + } + }; + this.mRegisterNew.setOnCheckedChangeListener(OnCheckedShowConfirmPassword); } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.editaccount, menu); - MenuItem showQrCode = menu.findItem(R.id.action_show_qr_code); + final MenuItem showQrCode = menu.findItem(R.id.action_show_qr_code); + final MenuItem showBlocklist = menu.findItem(R.id.action_show_block_list); + final MenuItem showMoreInfo = menu.findItem(R.id.action_server_info_show_more); + final MenuItem changePassword = menu.findItem(R.id.action_change_password_on_server); if (mAccount == null) { showQrCode.setVisible(false); + showBlocklist.setVisible(false); + showMoreInfo.setVisible(false); + changePassword.setVisible(false); + } else if (mAccount.getStatus() != Account.State.ONLINE) { + showBlocklist.setVisible(false); + showMoreInfo.setVisible(false); + changePassword.setVisible(false); + } else if (!mAccount.getXmppConnection().getFeatures().blocking()) { + showBlocklist.setVisible(false); } return true; } @@ -333,32 +348,38 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate protected void onStart() { super.onStart(); if (getIntent() != null) { - try { - this.jidToEdit = Jid.fromString(getIntent().getStringExtra("jid")); - } catch (final InvalidJidException | NullPointerException ignored) { - this.jidToEdit = null; - } - if (this.jidToEdit != null) { + try { + this.jidToEdit = Jid.fromString(getIntent().getStringExtra("jid")); + } catch (final InvalidJidException | NullPointerException ignored) { + this.jidToEdit = null; + } + if (this.jidToEdit != null) { this.mRegisterNew.setVisibility(View.GONE); - getActionBar().setTitle(getString(R.string.account_details)); + if (getActionBar() != null) { + getActionBar().setTitle(getString(R.string.account_details)); + } } else { this.mAvatar.setVisibility(View.GONE); - getActionBar().setTitle(R.string.action_add_account); + if (getActionBar() != null) { + getActionBar().setTitle(R.string.action_add_account); + } } } } @Override protected void onBackendConnected() { - KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, - android.R.layout.simple_list_item_1, - xmppConnectionService.getKnownHosts()); + final KnownHostsAdapter mKnownHostsAdapter = new KnownHostsAdapter(this, + android.R.layout.simple_list_item_1, + xmppConnectionService.getKnownHosts()); if (this.jidToEdit != null) { this.mAccount = xmppConnectionService.findAccountByJid(jidToEdit); updateAccountInformation(); } else if (this.xmppConnectionService.getAccounts().size() == 0) { - getActionBar().setDisplayHomeAsUpEnabled(false); - getActionBar().setDisplayShowHomeEnabled(false); + if (getActionBar() != null) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setDisplayShowHomeEnabled(false); + } this.mCancelButton.setEnabled(false); this.mCancelButton.setTextColor(getSecondaryTextColor()); } @@ -366,6 +387,27 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate updateSaveButton(); } + @Override + public boolean onOptionsItemSelected(final MenuItem item) { + switch (item.getItemId()) { + case R.id.action_show_block_list: + final Intent showBlocklistIntent = new Intent(this, BlocklistActivity.class); + showBlocklistIntent.putExtra("account", mAccount.getJid().toString()); + startActivity(showBlocklistIntent); + break; + case R.id.action_server_info_show_more: + mMoreTable.setVisibility(item.isChecked() ? View.GONE : View.VISIBLE); + item.setChecked(!item.isChecked()); + break; + case R.id.action_change_password_on_server: + final Intent changePasswordIntent = new Intent(this, ChangePasswordActivity.class); + changePasswordIntent.putExtra("account", mAccount.getJid().toString()); + startActivity(changePasswordIntent); + break; + } + return super.onOptionsItemSelected(item); + } + private void updateAccountInformation() { this.mAccountJid.setText(this.mAccount.getJid().toBareJid().toString()); this.mPassword.setText(this.mAccount.getPassword()); @@ -381,18 +423,36 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mRegisterNew.setVisibility(View.GONE); this.mRegisterNew.setChecked(false); } - if (this.mAccount.getStatus() == Account.State.ONLINE - && !this.mFetchingAvatar) { + if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { this.mStats.setVisibility(View.VISIBLE); - this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull( - getApplicationContext(), this.mAccount.getXmppConnection() - .getLastSessionEstablished())); + this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() + .getLastSessionEstablished())); Features features = this.mAccount.getXmppConnection().getFeatures(); + if (features.rosterVersioning()) { + this.mServerInfoRosterVersion.setText(R.string.server_info_available); + } else { + this.mServerInfoRosterVersion.setText(R.string.server_info_unavailable); + } if (features.carbons()) { this.mServerInfoCarbons.setText(R.string.server_info_available); } else { this.mServerInfoCarbons - .setText(R.string.server_info_unavailable); + .setText(R.string.server_info_unavailable); + } + if (features.mam()) { + this.mServerInfoMam.setText(R.string.server_info_available); + } else { + this.mServerInfoMam.setText(R.string.server_info_unavailable); + } + if (features.csi()) { + this.mServerInfoCSI.setText(R.string.server_info_available); + } else { + this.mServerInfoCSI.setText(R.string.server_info_unavailable); + } + if (features.blocking()) { + this.mServerInfoBlocking.setText(R.string.server_info_available); + } else { + this.mServerInfoBlocking.setText(R.string.server_info_unavailable); } if (features.sm()) { this.mServerInfoSm.setText(R.string.server_info_available); @@ -409,21 +469,21 @@ public class EditAccountActivity extends XmppActivity implements OnAccountUpdate this.mOtrFingerprintBox.setVisibility(View.VISIBLE); this.mOtrFingerprint.setText(CryptoHelper.prettifyFingerprint(fingerprint)); this.mOtrFingerprintToClipboardButton - .setVisibility(View.VISIBLE); + .setVisibility(View.VISIBLE); this.mOtrFingerprintToClipboardButton - .setOnClickListener(new View.OnClickListener() { + .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { + @Override + public void onClick(final View v) { - if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) { - Toast.makeText( - EditAccountActivity.this, - R.string.toast_message_otr_fingerprint, - Toast.LENGTH_SHORT).show(); - } + if (copyTextToClipboard(fingerprint, R.string.otr_fingerprint)) { + Toast.makeText( + EditAccountActivity.this, + R.string.toast_message_otr_fingerprint, + Toast.LENGTH_SHORT).show(); } - }); + } + }); } else { this.mOtrFingerprintBox.setVisibility(View.GONE); } diff --git a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java index 906a16cc..b3ab5ee6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ManageAccountActivity.java @@ -26,21 +26,25 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda protected Account selectedAccount = null; - protected List<Account> accountList = new ArrayList<Account>(); + protected final List<Account> accountList = new ArrayList<>(); protected ListView accountListView; protected AccountAdapter mAccountAdapter; - @Override - public void onAccountUpdate() { + + @Override + public void onAccountUpdate() { + synchronized (this.accountList) { accountList.clear(); accountList.addAll(xmppConnectionService.getAccounts()); - runOnUiThread(new Runnable() { - - @Override - public void run() { - mAccountAdapter.notifyDataSetChanged(); - } - }); } + runOnUiThread(new Runnable() { + + @Override + public void run() { + invalidateOptionsMenu(); + mAccountAdapter.notifyDataSetChanged(); + } + }); + } @Override protected void onCreate(Bundle savedInstanceState) { @@ -91,6 +95,14 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.manageaccounts, menu); + MenuItem enableAll = menu.findItem(R.id.action_enable_all); + if (!accountsLeftToEnable()) { + enableAll.setVisible(false); + } + MenuItem disableAll = menu.findItem(R.id.action_disable_all); + if (!accountsLeftToDisable()) { + disableAll.setVisible(false); + } return true; } @@ -120,12 +132,18 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.action_add_account: - startActivity(new Intent(getApplicationContext(), - EditAccountActivity.class)); - break; - default: - break; + case R.id.action_add_account: + startActivity(new Intent(getApplicationContext(), + EditAccountActivity.class)); + break; + case R.id.action_disable_all: + disableAllAccounts(); + break; + case R.id.action_enable_all: + enableAllAccounts(); + break; + default: + break; } return super.onOptionsItemSelected(item); } @@ -158,6 +176,56 @@ public class ManageAccountActivity extends XmppActivity implements OnAccountUpda startActivity(intent); } + private void disableAllAccounts() { + List<Account> list = new ArrayList<>(); + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + list.add(account); + } + } + } + for(Account account : list) { + disableAccount(account); + } + } + + private boolean accountsLeftToDisable() { + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (!account.isOptionSet(Account.OPTION_DISABLED)) { + return true; + } + } + return false; + } + } + + private boolean accountsLeftToEnable() { + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (account.isOptionSet(Account.OPTION_DISABLED)) { + return true; + } + } + return false; + } + } + + private void enableAllAccounts() { + List<Account> list = new ArrayList<>(); + synchronized (this.accountList) { + for (Account account : this.accountList) { + if (account.isOptionSet(Account.OPTION_DISABLED)) { + list.add(account); + } + } + } + for(Account account : list) { + enableAccount(account); + } + } + private void disableAccount(Account account) { account.setOption(Account.OPTION_DISABLED, true); xmppConnectionService.updateAccount(account); diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 609dc280..5e770376 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -1,17 +1,5 @@ package eu.siacs.conversations.ui; -import java.util.ArrayList; -import java.util.List; - -import eu.siacs.conversations.Config; -import eu.siacs.conversations.R; -import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.entities.Conversation; -import eu.siacs.conversations.entities.Message; -import eu.siacs.conversations.ui.adapter.ConversationAdapter; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; -import eu.siacs.conversations.xmpp.jid.Jid; - import android.app.PendingIntent; import android.content.Intent; import android.net.Uri; @@ -25,10 +13,27 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.ListView; import android.widget.Toast; +import java.io.UnsupportedEncodingException; +import java.net.URLConnection; +import java.net.URLDecoder; +import java.nio.charset.UnsupportedCharsetException; +import java.util.ArrayList; +import java.util.List; + +import eu.siacs.conversations.Config; +import eu.siacs.conversations.R; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Conversation; +import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.ui.adapter.ConversationAdapter; +import eu.siacs.conversations.xmpp.jid.InvalidJidException; +import eu.siacs.conversations.xmpp.jid.Jid; + public class ShareWithActivity extends XmppActivity { private class Share { public Uri uri; + public boolean image; public String account; public String contact; public String text; @@ -38,9 +43,9 @@ public class ShareWithActivity extends XmppActivity { private static final int REQUEST_START_NEW_CONVERSATION = 0x0501; private ListView mListView; - private List<Conversation> mConversations = new ArrayList<Conversation>(); + private List<Conversation> mConversations = new ArrayList<>(); - private UiCallback<Message> attachImageCallback = new UiCallback<Message>() { + private UiCallback<Message> attachFileCallback = new UiCallback<Message>() { @Override public void userInputRequried(PendingIntent pi, Message object) { @@ -78,11 +83,12 @@ public class ShareWithActivity extends XmppActivity { @Override protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - getActionBar().setDisplayHomeAsUpEnabled(false); - getActionBar().setHomeButtonEnabled(false); + if (getActionBar() != null) { + getActionBar().setDisplayHomeAsUpEnabled(false); + getActionBar().setHomeButtonEnabled(false); + } setContentView(R.layout.share_with); setTitle(getString(R.string.title_activity_sharewith)); @@ -114,10 +120,10 @@ public class ShareWithActivity extends XmppActivity { } @Override - public boolean onOptionsItemSelected(MenuItem item) { + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_add: - Intent intent = new Intent(getApplicationContext(), + final Intent intent = new Intent(getApplicationContext(), ChooseContactActivity.class); startActivityForResult(intent, REQUEST_START_NEW_CONVERSATION); return true; @@ -127,16 +133,20 @@ public class ShareWithActivity extends XmppActivity { @Override public void onStart() { - if (getIntent().getType() != null - && getIntent().getType().startsWith("image/")) { - this.share.uri = (Uri) getIntent().getParcelableExtra( - Intent.EXTRA_STREAM); + final String type = getIntent().getType(); + if (type != null && !type.startsWith("text/")) { + this.share.uri = (Uri) getIntent().getParcelableExtra(Intent.EXTRA_STREAM); + try { + this.share.image = type.startsWith("image/") + || URLConnection.guessContentTypeFromName(this.share.uri.toString()).startsWith("image/"); + } catch (final StringIndexOutOfBoundsException ignored) { + this.share.image = false; + } } else { this.share.text = getIntent().getStringExtra(Intent.EXTRA_TEXT); } if (xmppConnectionServiceBound) { - xmppConnectionService.populateWithOrderedConversations( - mConversations, this.share.uri == null); + xmppConnectionService.populateWithOrderedConversations(mConversations, this.share.uri == null); } super.onStart(); } @@ -177,12 +187,21 @@ public class ShareWithActivity extends XmppActivity { selectPresence(conversation, new OnPresenceSelected() { @Override public void onPresenceSelected() { - Toast.makeText(getApplicationContext(), - getText(R.string.preparing_image), - Toast.LENGTH_LONG).show(); - ShareWithActivity.this.xmppConnectionService + if (share.image) { + Toast.makeText(getApplicationContext(), + getText(R.string.preparing_image), + Toast.LENGTH_LONG).show(); + ShareWithActivity.this.xmppConnectionService .attachImageToConversation(conversation, share.uri, - attachImageCallback); + attachFileCallback); + } else { + Toast.makeText(getApplicationContext(), + getText(R.string.preparing_file), + Toast.LENGTH_LONG).show(); + ShareWithActivity.this.xmppConnectionService + .attachFileToConversation(conversation, share.uri, + attachFileCallback); + } switchToConversation(conversation, null, true); finish(); } @@ -195,4 +214,4 @@ public class ShareWithActivity extends XmppActivity { } -}
\ No newline at end of file +} diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index c8b41821..209c0a7b 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -38,6 +38,7 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.ArrayAdapter; import android.widget.AutoCompleteTextView; import android.widget.CheckBox; +import android.widget.Checkable; import android.widget.EditText; import android.widget.ListView; import android.widget.Spinner; @@ -53,19 +54,20 @@ import java.util.List; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.entities.Blockable; import eu.siacs.conversations.entities.Bookmark; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.ListItem; -import eu.siacs.conversations.utils.XmppUri; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; import eu.siacs.conversations.ui.adapter.ListItemAdapter; -import eu.siacs.conversations.utils.Validator; +import eu.siacs.conversations.utils.XmppUri; +import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; -public class StartConversationActivity extends XmppActivity implements OnRosterUpdate { +public class StartConversationActivity extends XmppActivity implements OnRosterUpdate, OnUpdateBlocklist { public int conference_context_id; public int contact_context_id; @@ -133,7 +135,9 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU private ViewPager.SimpleOnPageChangeListener mOnPageChangeListener = new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { - getActionBar().setSelectedNavigationItem(position); + if (getActionBar() != null) { + getActionBar().setSelectedNavigationItem(position); + } onTabChanged(); } }; @@ -146,16 +150,27 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU @Override public void beforeTextChanged(CharSequence s, int start, int count, - int after) { + int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, - int count) { + int count) { } }; private MenuItem mMenuSearchView; private String mInitialJid; + private ListItemAdapter.OnTagClickedListener mOnTagClickedListener = new ListItemAdapter.OnTagClickedListener() { + @Override + public void onTagClicked(String tag) { + if (mMenuSearchView != null) { + mMenuSearchView.expandActionView(); + mSearchEditText.setText(""); + mSearchEditText.append(tag); + filter(tag); + } + } + }; @Override public void onRosterUpdate() { @@ -179,9 +194,9 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); mContactsTab = actionBar.newTab().setText(R.string.contacts) - .setTabListener(mTabListener); + .setTabListener(mTabListener); mConferencesTab = actionBar.newTab().setText(R.string.conferences) - .setTabListener(mTabListener); + .setTabListener(mTabListener); actionBar.addTab(mContactsTab); actionBar.addTab(mConferencesTab); @@ -207,35 +222,36 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU mConferenceListFragment.setListAdapter(mConferenceAdapter); mConferenceListFragment.setContextMenu(R.menu.conference_context); mConferenceListFragment - .setOnListItemClickListener(new OnItemClickListener() { + .setOnListItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> arg0, View arg1, - int position, long arg3) { - openConversationForBookmark(position); - } - }); + @Override + public void onItemClick(AdapterView<?> arg0, View arg1, + int position, long arg3) { + openConversationForBookmark(position); + } + }); mContactsAdapter = new ListItemAdapter(this, contacts); + ((ListItemAdapter) mContactsAdapter).setOnTagClickedListener(this.mOnTagClickedListener); mContactsListFragment.setListAdapter(mContactsAdapter); mContactsListFragment.setContextMenu(R.menu.contact_context); mContactsListFragment - .setOnListItemClickListener(new OnItemClickListener() { + .setOnListItemClickListener(new OnItemClickListener() { - @Override - public void onItemClick(AdapterView<?> arg0, View arg1, - int position, long arg3) { - openConversationForContact(position); - } - }); + @Override + public void onItemClick(AdapterView<?> arg0, View arg1, + int position, long arg3) { + openConversationForContact(position); + } + }); } protected void openConversationForContact(int position) { Contact contact = (Contact) contacts.get(position); Conversation conversation = xmppConnectionService - .findOrCreateConversation(contact.getAccount(), - contact.getJid(), false); + .findOrCreateConversation(contact.getAccount(), + contact.getJid(), false); switchToConversation(conversation); } @@ -251,8 +267,8 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU protected void openConversationForBookmark(int position) { Bookmark bookmark = (Bookmark) conferences.get(position); Conversation conversation = xmppConnectionService - .findOrCreateConversation(bookmark.getAccount(), - bookmark.getJid(), true); + .findOrCreateConversation(bookmark.getAccount(), + bookmark.getJid(), true); conversation.setBookmark(bookmark); if (!conversation.getMucOptions().online()) { xmppConnectionService.joinMuc(conversation); @@ -270,14 +286,19 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU switchToContactDetails(contact); } + protected void toggleContactBlock() { + final int position = contact_context_id; + BlockContactDialog.show(this, xmppConnectionService, (Contact)contacts.get(position)); + } + protected void deleteContact() { - int position = contact_context_id; + final int position = contact_context_id; final Contact contact = (Contact) contacts.get(position); - AlertDialog.Builder builder = new AlertDialog.Builder(this); + final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.action_delete_contact); builder.setMessage(getString(R.string.remove_contact_text, - contact.getJid())); + contact.getJid())); builder.setPositiveButton(R.string.delete, new OnClickListener() { @Override @@ -287,7 +308,6 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } }); builder.create().show(); - } protected void deleteConference() { @@ -298,7 +318,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU builder.setNegativeButton(R.string.cancel, null); builder.setTitle(R.string.delete_bookmark); builder.setMessage(getString(R.string.remove_bookmark_text, - bookmark.getJid())); + bookmark.getJid())); builder.setPositiveButton(R.string.delete, new OnClickListener() { @Override @@ -318,13 +338,10 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU protected void showCreateContactDialog(final String prefilledJid, final String fingerprint) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.create_contact); - View dialogView = getLayoutInflater().inflate( - R.layout.create_contact_dialog, null); + View dialogView = getLayoutInflater().inflate(R.layout.create_contact_dialog, null); final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); - final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView - .findViewById(R.id.jid); - jid.setAdapter(new KnownHostsAdapter(this, - android.R.layout.simple_list_item_1, mKnownHosts)); + final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid); + jid.setAdapter(new KnownHostsAdapter(this,android.R.layout.simple_list_item_1, mKnownHosts)); if (prefilledJid != null) { jid.append(prefilledJid); if (fingerprint!=null) { @@ -344,41 +361,37 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { if (!xmppConnectionServiceBound) { return; } - if (Validator.isValidJid(jid.getText().toString())) { - final Jid accountJid; - try { - accountJid = Jid.fromString((String) spinner - .getSelectedItem()); - } catch (final InvalidJidException e) { - return; - } - final Jid contactJid; - try { - contactJid = Jid.fromString(jid.getText().toString()); - } catch (final InvalidJidException e) { - return; - } - Account account = xmppConnectionService - .findAccountByJid(accountJid); - if (account == null) { - dialog.dismiss(); - return; - } - Contact contact = account.getRoster().getContact(contactJid); - if (contact.showInRoster()) { - jid.setError(getString(R.string.contact_already_exists)); - } else { - contact.addOtrFingerprint(fingerprint); - xmppConnectionService.createContact(contact); - dialog.dismiss(); - switchToConversation(contact); - } - } else { + final Jid accountJid; + try { + accountJid = Jid.fromString((String) spinner.getSelectedItem()); + } catch (final InvalidJidException e) { + return; + } + final Jid contactJid; + try { + contactJid = Jid.fromString(jid.getText().toString()); + } catch (final InvalidJidException e) { jid.setError(getString(R.string.invalid_jid)); + return; + } + final Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + final Contact contact = account.getRoster().getContact(contactJid); + if (contact.showInRoster()) { + jid.setError(getString(R.string.contact_already_exists)); + } else { + contact.addOtrFingerprint(fingerprint); + xmppConnectionService.createContact(contact); + dialog.dismiss(); + switchToConversation(contact); } } }); @@ -386,22 +399,19 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } @SuppressLint("InflateParams") - protected void showJoinConferenceDialog(String prefilledJid) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); + protected void showJoinConferenceDialog(final String prefilledJid) { + final AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.join_conference); - View dialogView = getLayoutInflater().inflate( - R.layout.join_conference_dialog, null); + final View dialogView = getLayoutInflater().inflate(R.layout.join_conference_dialog, null); final Spinner spinner = (Spinner) dialogView.findViewById(R.id.account); - final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView - .findViewById(R.id.jid); - jid.setAdapter(new KnownHostsAdapter(this, - android.R.layout.simple_list_item_1, mKnownConferenceHosts)); + final AutoCompleteTextView jid = (AutoCompleteTextView) dialogView.findViewById(R.id.jid); + jid.setAdapter(new KnownHostsAdapter(this,android.R.layout.simple_list_item_1, mKnownConferenceHosts)); if (prefilledJid != null) { jid.append(prefilledJid); } populateAccountSpinner(spinner); - final CheckBox bookmarkCheckBox = (CheckBox) dialogView - .findViewById(R.id.bookmark); + final Checkable bookmarkCheckBox = (CheckBox) dialogView + .findViewById(R.id.bookmark); builder.setView(dialogView); builder.setNegativeButton(R.string.cancel, null); builder.setPositiveButton(R.string.join, null); @@ -411,62 +421,59 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU new View.OnClickListener() { @Override - public void onClick(View v) { + public void onClick(final View v) { if (!xmppConnectionServiceBound) { return; } - if (Validator.isValidJid(jid.getText().toString())) { - final Jid accountJid; - try { - accountJid = Jid.fromString((String) spinner.getSelectedItem()); - } catch (final InvalidJidException e) { - return; - } - final Jid conferenceJid; - try { - conferenceJid = Jid.fromString(jid.getText().toString()); - } catch (final InvalidJidException e) { - return; // TODO: Do some error handling... - } - Account account = xmppConnectionService - .findAccountByJid(accountJid); - if (account == null) { - dialog.dismiss(); - return; - } - if (bookmarkCheckBox.isChecked()) { - if (account.hasBookmarkFor(conferenceJid)) { - jid.setError(getString(R.string.bookmark_already_exists)); - } else { - Bookmark bookmark = new Bookmark(account, - conferenceJid); - bookmark.setAutojoin(true); - account.getBookmarks().add(bookmark); - xmppConnectionService - .pushBookmarks(account); - Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, - conferenceJid, true); - conversation.setBookmark(bookmark); - if (!conversation.getMucOptions().online()) { - xmppConnectionService - .joinMuc(conversation); - } - dialog.dismiss(); - switchToConversation(conversation); - } + final Jid accountJid; + try { + accountJid = Jid.fromString((String) spinner.getSelectedItem()); + } catch (final InvalidJidException e) { + return; + } + final Jid conferenceJid; + try { + conferenceJid = Jid.fromString(jid.getText().toString()); + } catch (final InvalidJidException e) { + jid.setError(getString(R.string.invalid_jid)); + return; + } + final Account account = xmppConnectionService + .findAccountByJid(accountJid); + if (account == null) { + dialog.dismiss(); + return; + } + if (bookmarkCheckBox.isChecked()) { + if (account.hasBookmarkFor(conferenceJid)) { + jid.setError(getString(R.string.bookmark_already_exists)); } else { - Conversation conversation = xmppConnectionService - .findOrCreateConversation(account, - conferenceJid, true); + final Bookmark bookmark = new Bookmark(account, + conferenceJid); + bookmark.setAutojoin(true); + account.getBookmarks().add(bookmark); + xmppConnectionService + .pushBookmarks(account); + final Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + conversation.setBookmark(bookmark); if (!conversation.getMucOptions().online()) { - xmppConnectionService.joinMuc(conversation); + xmppConnectionService + .joinMuc(conversation); } dialog.dismiss(); switchToConversation(conversation); } } else { - jid.setError(getString(R.string.invalid_jid)); + final Conversation conversation = xmppConnectionService + .findOrCreateConversation(account, + conferenceJid, true); + if (!conversation.getMucOptions().online()) { + xmppConnectionService.joinMuc(conversation); + } + dialog.dismiss(); + switchToConversation(conversation); } } }); @@ -474,8 +481,8 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU protected void switchToConversation(Contact contact) { Conversation conversation = xmppConnectionService - .findOrCreateConversation(contact.getAccount(), - contact.getJid(), false); + .findOrCreateConversation(contact.getAccount(), + contact.getJid(), false); switchToConversation(conversation); } @@ -491,14 +498,14 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU this.mOptionsMenu = menu; getMenuInflater().inflate(R.menu.start_conversation, menu); MenuItem menuCreateContact = menu - .findItem(R.id.action_create_contact); + .findItem(R.id.action_create_contact); MenuItem menuCreateConference = menu - .findItem(R.id.action_join_conference); + .findItem(R.id.action_join_conference); mMenuSearchView = menu.findItem(R.id.action_search); mMenuSearchView.setOnActionExpandListener(mOnActionExpandListener); View mSearchView = mMenuSearchView.getActionView(); mSearchEditText = (EditText) mSearchView - .findViewById(R.id.search_field); + .findViewById(R.id.search_field); mSearchEditText.addTextChangedListener(mSearchTextWatcher); if (getActionBar().getSelectedNavigationIndex() == 0) { menuCreateConference.setVisible(false); @@ -567,7 +574,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } this.mKnownHosts = xmppConnectionService.getKnownHosts(); this.mKnownConferenceHosts = xmppConnectionService - .getKnownConferenceHosts(); + .getKnownConferenceHosts(); if (this.mPendingInvite != null) { mPendingInvite.invite(); this.mPendingInvite = null; @@ -609,7 +616,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU byte[] payload = record.getPayload(); if (payload[0] == 0) { return new Invite(Uri.parse(new String(Arrays.copyOfRange( - payload, 1, payload.length)))).invite(); + payload, 1, payload.length)))).invite(); } } } @@ -690,16 +697,29 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU invalidateOptionsMenu(); } + @Override + public void OnUpdateBlocklist(final Status status) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (mSearchEditText != null) { + filter(mSearchEditText.getText().toString()); + } + } + }); + } + public static class MyListFragment extends ListFragment { private AdapterView.OnItemClickListener mOnItemClickListener; private int mResContextMenu; - public void setContextMenu(int res) { + public void setContextMenu(final int res) { this.mResContextMenu = res; } @Override - public void onListItemClick(ListView l, View v, int position, long id) { + public void onListItemClick(final ListView l, final View v, final int position, final long id) { if (mOnItemClickListener != null) { mOnItemClickListener.onItemClick(l, v, position, id); } @@ -710,28 +730,38 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU } @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(final View view, final Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); registerForContextMenu(getListView()); getListView().setFastScrollEnabled(true); } @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { + public void onCreateContextMenu(final ContextMenu menu, final View v, + final ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); - StartConversationActivity activity = (StartConversationActivity) getActivity(); + final StartConversationActivity activity = (StartConversationActivity) getActivity(); activity.getMenuInflater().inflate(mResContextMenu, menu); - AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; + final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; if (mResContextMenu == R.menu.conference_context) { activity.conference_context_id = acmi.position; } else { activity.contact_context_id = acmi.position; + final Blockable contact = (Contact) activity.contacts.get(acmi.position); + + final MenuItem blockUnblockItem = menu.findItem(R.id.context_contact_block_unblock); + if (blockUnblockItem != null) { + if (contact.isBlocked()) { + blockUnblockItem.setTitle(R.string.unblock_contact); + } else { + blockUnblockItem.setTitle(R.string.block_contact); + } + } } } @Override - public boolean onContextItemSelected(MenuItem item) { + public boolean onContextItemSelected(final MenuItem item) { StartConversationActivity activity = (StartConversationActivity) getActivity(); switch (item.getItemId()) { case R.id.context_start_conversation: @@ -740,6 +770,9 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU case R.id.context_contact_details: activity.openDetailsForContact(); break; + case R.id.context_contact_block_unblock: + activity.toggleContactBlock(); + break; case R.id.context_delete_contact: activity.deleteContact(); break; @@ -755,11 +788,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU private class Invite extends XmppUri { - public Invite(Uri uri) { + public Invite(final Uri uri) { super(uri); } - public Invite(String uri) { + public Invite(final String uri) { super(uri); } diff --git a/src/main/java/eu/siacs/conversations/ui/TimePreference.java b/src/main/java/eu/siacs/conversations/ui/TimePreference.java new file mode 100644 index 00000000..e32b068c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/ui/TimePreference.java @@ -0,0 +1,105 @@ +package eu.siacs.conversations.ui; + +import android.content.Context; +import android.content.res.TypedArray; +import android.preference.DialogPreference; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TimePicker; + +import java.text.DateFormat; +import java.util.Calendar; +import java.util.Date; + +public class TimePreference extends DialogPreference implements Preference.OnPreferenceChangeListener { + private TimePicker picker = null; + public final static long DEFAULT_VALUE = 0; + + public TimePreference(final Context context, final AttributeSet attrs) { + super(context, attrs, 0); + this.setOnPreferenceChangeListener(this); + } + + protected void setTime(final long time) { + persistLong(time); + notifyDependencyChange(shouldDisableDependents()); + notifyChanged(); + } + + protected void updateSummary(final long time) { + final DateFormat dateFormat = android.text.format.DateFormat.getTimeFormat(getContext()); + final Date date = new Date(time); + setSummary(dateFormat.format(date.getTime())); + } + + @Override + protected View onCreateDialogView() { + picker = new TimePicker(getContext()); + picker.setIs24HourView(android.text.format.DateFormat.is24HourFormat(getContext())); + return picker; + } + + protected Calendar getPersistedTime() { + final Calendar c = Calendar.getInstance(); + c.setTimeInMillis(getPersistedLong(DEFAULT_VALUE)); + + return c; + } + + @SuppressWarnings("NullableProblems") + @Override + protected void onBindDialogView(final View v) { + super.onBindDialogView(v); + final Calendar c = getPersistedTime(); + + picker.setCurrentHour(c.get(Calendar.HOUR_OF_DAY)); + picker.setCurrentMinute(c.get(Calendar.MINUTE)); + } + + @Override + protected void onDialogClosed(final boolean positiveResult) { + super.onDialogClosed(positiveResult); + + if (positiveResult) { + final Calendar c = Calendar.getInstance(); + c.set(Calendar.MINUTE, picker.getCurrentMinute()); + c.set(Calendar.HOUR_OF_DAY, picker.getCurrentHour()); + + + if (!callChangeListener(c.getTimeInMillis())) { + return; + } + + setTime(c.getTimeInMillis()); + } + } + + @Override + protected Object onGetDefaultValue(final TypedArray a, final int index) { + return a.getInteger(index, 0); + } + + @Override + protected void onSetInitialValue(final boolean restorePersistedValue, final Object defaultValue) { + long time; + if (defaultValue == null) { + time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE; + } else if (defaultValue instanceof Long) { + time = restorePersistedValue ? getPersistedLong((Long) defaultValue) : (Long) defaultValue; + } else if (defaultValue instanceof Calendar) { + time = restorePersistedValue ? getPersistedLong(((Calendar)defaultValue).getTimeInMillis()) : ((Calendar)defaultValue).getTimeInMillis(); + } else { + time = restorePersistedValue ? getPersistedLong(DEFAULT_VALUE) : DEFAULT_VALUE; + } + + setTime(time); + updateSummary(time); + } + + @Override + public boolean onPreferenceChange(final Preference preference, final Object newValue) { + ((TimePreference) preference).updateSummary((Long)newValue); + return true; + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java index e5775ab0..c33decd8 100644 --- a/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/VerifyOTRActivity.java @@ -1,15 +1,15 @@ package eu.siacs.conversations.ui; +import android.app.ActionBar; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.view.Menu; -import android.view.MenuItem; import android.view.View; import android.widget.Button; import android.widget.EditText; -import android.widget.RelativeLayout; +import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; @@ -32,57 +32,56 @@ import eu.siacs.conversations.xmpp.jid.Jid; public class VerifyOTRActivity extends XmppActivity implements XmppConnectionService.OnConversationUpdate { public static final String ACTION_VERIFY_CONTACT = "verify_contact"; + public static final int MODE_SCAN_FINGERPRINT = - 0x0502; + public static final int MODE_ASK_QUESTION = 0x0503; + public static final int MODE_ANSWER_QUESTION = 0x0504; + public static final int MODE_MANUAL_VERIFICATION = 0x0505; - private RelativeLayout mVerificationAreaOne; - private RelativeLayout mVerificationAreaTwo; - private TextView mErrorNoSession; - private TextView mRemoteJid; + private LinearLayout mManualVerificationArea; + private LinearLayout mSmpVerificationArea; private TextView mRemoteFingerprint; private TextView mYourFingerprint; - private EditText mSharedSecretHint; - private EditText mSharedSecretSecret; - private Button mButtonScanQrCode; - private Button mButtonShowQrCode; - private Button mButtonSharedSecretPositive; - private Button mButtonSharedSecretNegative; + private TextView mVerificationExplain; private TextView mStatusMessage; + private TextView mSharedSecretHint; + private EditText mSharedSecretHintEditable; + private EditText mSharedSecretSecret; + private Button mLeftButton; + private Button mRightButton; private Account mAccount; private Conversation mConversation; + private int mode = MODE_MANUAL_VERIFICATION; + private XmppUri mPendingUri = null; private DialogInterface.OnClickListener mVerifyFingerprintListener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int click) { mConversation.verifyOtrFingerprint(); - updateView(); xmppConnectionService.syncRosterToDisk(mConversation.getAccount()); + Toast.makeText(VerifyOTRActivity.this,R.string.verified,Toast.LENGTH_SHORT).show(); + finish(); } }; - private View.OnClickListener mShowQrCodeListener = new View.OnClickListener() { - @Override - public void onClick(final View view) { - showQrCode(); - } - }; - - private View.OnClickListener mScanQrCodeListener = new View.OnClickListener() { - - @Override - public void onClick(View view) { - new IntentIntegrator(VerifyOTRActivity.this).initiateScan(); - } - - }; - private View.OnClickListener mCreateSharedSecretListener = new View.OnClickListener() { @Override public void onClick(final View view) { if (isAccountOnline()) { - final String question = mSharedSecretHint.getText().toString(); + final String question = mSharedSecretHintEditable.getText().toString(); final String secret = mSharedSecretSecret.getText().toString(); - initSmp(question, secret); - updateView(); + if (question.trim().isEmpty()) { + mSharedSecretHintEditable.requestFocus(); + mSharedSecretHintEditable.setError(getString(R.string.shared_secret_hint_should_not_be_empty)); + } else if (secret.trim().isEmpty()) { + mSharedSecretSecret.requestFocus(); + mSharedSecretSecret.setError(getString(R.string.shared_secret_can_not_be_empty)); + } else { + mSharedSecretSecret.setError(null); + mSharedSecretHintEditable.setError(null); + initSmp(question, secret); + updateView(); + } } } }; @@ -100,7 +99,7 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer @Override public void onClick(View view) { if (isAccountOnline()) { - final String question = mSharedSecretHint.getText().toString(); + final String question = mSharedSecretHintEditable.getText().toString(); final String secret = mSharedSecretSecret.getText().toString(); respondSmp(question, secret); updateView(); @@ -124,14 +123,14 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer } }; - private XmppUri mPendingUri = null; - protected boolean initSmp(final String question, final String secret) { final Session session = mConversation.getOtrSession(); if (session!=null) { try { session.initSmp(question, secret); mConversation.smp().status = Conversation.Smp.STATUS_WE_REQUESTED; + mConversation.smp().secret = secret; + mConversation.smp().hint = question; return true; } catch (OtrException e) { return false; @@ -172,15 +171,17 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer } } - protected void verifyWithUri(XmppUri uri) { + protected boolean verifyWithUri(XmppUri uri) { Contact contact = mConversation.getContact(); if (this.mConversation.getContact().getJid().equals(uri.getJid()) && uri.getFingerprint() != null) { contact.addOtrFingerprint(uri.getFingerprint()); Toast.makeText(this,R.string.verified,Toast.LENGTH_SHORT).show(); updateView(); xmppConnectionService.syncRosterToDisk(contact.getAccount()); + return true; } else { Toast.makeText(this,R.string.could_not_verify_fingerprint,Toast.LENGTH_SHORT).show(); + return false; } } @@ -194,7 +195,7 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer } protected boolean handleIntent(Intent intent) { - if (intent.getAction().equals(ACTION_VERIFY_CONTACT)) { + if (intent != null && intent.getAction().equals(ACTION_VERIFY_CONTACT)) { try { this.mAccount = this.xmppConnectionService.findAccountByJid(Jid.fromString(intent.getExtras().getString("account"))); } catch (final InvalidJidException ignored) { @@ -208,6 +209,11 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer } catch (final InvalidJidException ignored) { return false; } + this.mode = intent.getIntExtra("mode", MODE_MANUAL_VERIFICATION); + if (this.mode == MODE_SCAN_FINGERPRINT) { + new IntentIntegrator(this).initiateScan(); + return false; + } return true; } else { return false; @@ -223,9 +229,12 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer XmppUri uri = new XmppUri(data); if (xmppConnectionServiceBound) { verifyWithUri(uri); + finish(); } else { this.mPendingUri = uri; } + } else { + finish(); } } super.onActivityResult(requestCode, requestCode, intent); @@ -234,84 +243,139 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer @Override protected void onBackendConnected() { if (handleIntent(getIntent())) { - if (mPendingUri!=null) { - verifyWithUri(mPendingUri); - mPendingUri = null; - } updateView(); + } else if (mPendingUri!=null) { + verifyWithUri(mPendingUri); + finish(); + mPendingUri = null; } + setIntent(null); } protected void updateView() { if (this.mConversation.hasValidOtrSession()) { + final ActionBar actionBar = getActionBar(); + this.mVerificationExplain.setText(R.string.no_otr_session_found); invalidateOptionsMenu(); - this.mVerificationAreaOne.setVisibility(View.VISIBLE); - this.mVerificationAreaTwo.setVisibility(View.VISIBLE); - this.mErrorNoSession.setVisibility(View.GONE); - this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint())); - this.mRemoteFingerprint.setText(this.mConversation.getOtrFingerprint()); - this.mRemoteJid.setText(this.mConversation.getContact().getJid().toBareJid().toString()); - Conversation.Smp smp = mConversation.smp(); - Session session = mConversation.getOtrSession(); - if (mConversation.isOtrFingerprintVerified()) { - deactivateButton(mButtonScanQrCode, R.string.verified); - } else { - activateButton(mButtonScanQrCode, R.string.scan_qr_code, mScanQrCodeListener); + switch(this.mode) { + case MODE_ASK_QUESTION: + if (actionBar != null ) { + actionBar.setTitle(R.string.ask_question); + } + this.updateViewAskQuestion(); + break; + case MODE_ANSWER_QUESTION: + if (actionBar != null ) { + actionBar.setTitle(R.string.smp_requested); + } + this.updateViewAnswerQuestion(); + break; + case MODE_MANUAL_VERIFICATION: + default: + if (actionBar != null ) { + actionBar.setTitle(R.string.manually_verify); + } + this.updateViewManualVerification(); + break; } - if (smp.status == Conversation.Smp.STATUS_NONE) { - activateButton(mButtonSharedSecretPositive, R.string.create, mCreateSharedSecretListener); - deactivateButton(mButtonSharedSecretNegative, R.string.cancel); - this.mSharedSecretHint.setFocusableInTouchMode(true); - this.mSharedSecretSecret.setFocusableInTouchMode(true); - this.mSharedSecretSecret.setText(""); - this.mSharedSecretHint.setText(""); - this.mSharedSecretHint.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); + } else { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.GONE); + } + } + + protected void updateViewManualVerification() { + this.mVerificationExplain.setText(R.string.manual_verification_explanation); + this.mManualVerificationArea.setVisibility(View.VISIBLE); + this.mSmpVerificationArea.setVisibility(View.GONE); + this.mYourFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mAccount.getOtrFingerprint())); + this.mRemoteFingerprint.setText(CryptoHelper.prettifyFingerprint(this.mConversation.getOtrFingerprint())); + if (this.mConversation.isOtrFingerprintVerified()) { + deactivateButton(this.mRightButton,R.string.verified); + activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); + } else { + activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); + activateButton(this.mRightButton,R.string.verify, new View.OnClickListener() { + @Override + public void onClick(View view) { + showManuallyVerifyDialog(); + } + }); + } + } + + protected void updateViewAskQuestion() { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.VISIBLE); + this.mVerificationExplain.setText(R.string.smp_explain_question); + final int smpStatus = this.mConversation.smp().status; + switch (smpStatus) { + case Conversation.Smp.STATUS_WE_REQUESTED: this.mStatusMessage.setVisibility(View.GONE); - } else if (smp.status == Conversation.Smp.STATUS_CONTACT_REQUESTED) { - this.mSharedSecretHint.setFocusable(false); - this.mSharedSecretHint.setText(smp.hint); - this.mSharedSecretSecret.setFocusableInTouchMode(true); - this.mSharedSecretHint.setVisibility(View.VISIBLE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mSharedSecretHintEditable.setText(this.mConversation.smp().hint); + this.mSharedSecretSecret.setText(this.mConversation.smp().secret); + this.activateButton(this.mLeftButton, R.string.cancel, this.mCancelSharedSecretListener); + this.deactivateButton(this.mRightButton, R.string.in_progress); + break; + case Conversation.Smp.STATUS_FAILED: this.mStatusMessage.setVisibility(View.GONE); - deactivateButton(mButtonSharedSecretNegative, R.string.cancel); - activateButton(mButtonSharedSecretPositive, R.string.respond, mRespondSharedSecretListener); - } else if (smp.status == Conversation.Smp.STATUS_FAILED) { - activateButton(mButtonSharedSecretNegative, R.string.cancel, mFinishListener); - activateButton(mButtonSharedSecretPositive, R.string.try_again, mRetrySharedSecretListener); - this.mSharedSecretHint.setVisibility(View.GONE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.requestFocus(); + this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); + this.deactivateButton(this.mLeftButton, R.string.cancel); + this.activateButton(this.mRightButton, R.string.try_again, this.mRetrySharedSecretListener); + break; + case Conversation.Smp.STATUS_VERIFIED: + this.mSharedSecretHintEditable.setText(""); + this.mSharedSecretHintEditable.setVisibility(View.GONE); + this.mSharedSecretSecret.setText(""); this.mSharedSecretSecret.setVisibility(View.GONE); this.mStatusMessage.setVisibility(View.VISIBLE); - this.mStatusMessage.setText(R.string.secrets_do_not_match); - this.mStatusMessage.setTextColor(getWarningTextColor()); - } else if (smp.status == Conversation.Smp.STATUS_FINISHED) { - this.mSharedSecretHint.setText(""); + this.deactivateButton(this.mLeftButton, R.string.cancel); + this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); + break; + default: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHintEditable.setVisibility(View.VISIBLE); + this.mSharedSecretSecret.setVisibility(View.VISIBLE); + this.activateButton(this.mLeftButton,R.string.cancel,this.mFinishListener); + this.activateButton(this.mRightButton, R.string.ask_question, this.mCreateSharedSecretListener); + break; + } + } + + protected void updateViewAnswerQuestion() { + this.mManualVerificationArea.setVisibility(View.GONE); + this.mSmpVerificationArea.setVisibility(View.VISIBLE); + this.mVerificationExplain.setText(R.string.smp_explain_answer); + this.mSharedSecretHintEditable.setVisibility(View.GONE); + this.mSharedSecretHint.setVisibility(View.VISIBLE); + this.deactivateButton(this.mLeftButton, R.string.cancel); + final int smpStatus = this.mConversation.smp().status; + switch (smpStatus) { + case Conversation.Smp.STATUS_CONTACT_REQUESTED: + this.mStatusMessage.setVisibility(View.GONE); + this.mSharedSecretHint.setText(this.mConversation.smp().hint); + this.activateButton(this.mRightButton,R.string.respond,this.mRespondSharedSecretListener); + break; + case Conversation.Smp.STATUS_VERIFIED: + this.mSharedSecretHintEditable.setText(""); + this.mSharedSecretHintEditable.setVisibility(View.GONE); this.mSharedSecretHint.setVisibility(View.GONE); this.mSharedSecretSecret.setText(""); this.mSharedSecretSecret.setVisibility(View.GONE); this.mStatusMessage.setVisibility(View.VISIBLE); - this.mStatusMessage.setTextColor(getPrimaryColor()); - deactivateButton(mButtonSharedSecretNegative, R.string.cancel); - if (mConversation.isOtrFingerprintVerified()) { - activateButton(mButtonSharedSecretPositive, R.string.finish, mFinishListener); - this.mStatusMessage.setText(R.string.verified); - } else { - activateButton(mButtonSharedSecretPositive,R.string.reset,mRetrySharedSecretListener); - this.mStatusMessage.setText(R.string.secret_accepted); - } - } else if (session != null && session.isSmpInProgress()) { - deactivateButton(mButtonSharedSecretPositive, R.string.in_progress); - activateButton(mButtonSharedSecretNegative, R.string.cancel, mCancelSharedSecretListener); - this.mSharedSecretHint.setVisibility(View.VISIBLE); - this.mSharedSecretSecret.setVisibility(View.VISIBLE); - this.mSharedSecretHint.setFocusable(false); - this.mSharedSecretSecret.setFocusable(false); - } - } else { - this.mVerificationAreaOne.setVisibility(View.GONE); - this.mVerificationAreaTwo.setVisibility(View.GONE); - this.mErrorNoSession.setVisibility(View.VISIBLE); + this.activateButton(this.mRightButton, R.string.finish, this.mFinishListener); + break; + case Conversation.Smp.STATUS_FAILED: + default: + this.mSharedSecretSecret.requestFocus(); + this.mSharedSecretSecret.setError(getString(R.string.secrets_do_not_match)); + this.activateButton(this.mRightButton,R.string.finish,this.mFinishListener); + break; } } @@ -334,41 +398,25 @@ public class VerifyOTRActivity extends XmppActivity implements XmppConnectionSer super.onCreate(savedInstanceState); setContentView(R.layout.activity_verify_otr); this.mRemoteFingerprint = (TextView) findViewById(R.id.remote_fingerprint); - this.mRemoteJid = (TextView) findViewById(R.id.remote_jid); this.mYourFingerprint = (TextView) findViewById(R.id.your_fingerprint); - this.mButtonSharedSecretNegative = (Button) findViewById(R.id.button_shared_secret_negative); - this.mButtonSharedSecretPositive = (Button) findViewById(R.id.button_shared_secret_positive); - this.mButtonScanQrCode = (Button) findViewById(R.id.button_scan_qr_code); - this.mButtonShowQrCode = (Button) findViewById(R.id.button_show_qr_code); - this.mButtonShowQrCode.setOnClickListener(this.mShowQrCodeListener); + this.mLeftButton = (Button) findViewById(R.id.left_button); + this.mRightButton = (Button) findViewById(R.id.right_button); + this.mVerificationExplain = (TextView) findViewById(R.id.verification_explanation); + this.mStatusMessage = (TextView) findViewById(R.id.status_message); this.mSharedSecretSecret = (EditText) findViewById(R.id.shared_secret_secret); - this.mSharedSecretHint = (EditText) findViewById(R.id.shared_secret_hint); - this.mStatusMessage= (TextView) findViewById(R.id.status_message); - this.mVerificationAreaOne = (RelativeLayout) findViewById(R.id.verification_area_one); - this.mVerificationAreaTwo = (RelativeLayout) findViewById(R.id.verification_area_two); - this.mErrorNoSession = (TextView) findViewById(R.id.error_no_session); + this.mSharedSecretHintEditable = (EditText) findViewById(R.id.shared_secret_hint_editable); + this.mSharedSecretHint = (TextView) findViewById(R.id.shared_secret_hint); + this.mManualVerificationArea = (LinearLayout) findViewById(R.id.manual_verification_area); + this.mSmpVerificationArea = (LinearLayout) findViewById(R.id.smp_verification_area); } @Override - public boolean onCreateOptionsMenu(Menu menu) { + public boolean onCreateOptionsMenu(final Menu menu) { + super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.verify_otr, menu); - if (mConversation != null && mConversation.isOtrFingerprintVerified()) { - MenuItem manuallyVerifyItem = menu.findItem(R.id.manually_verify); - manuallyVerifyItem.setVisible(false); - } return true; } - @Override - public boolean onOptionsItemSelected(MenuItem menuItem) { - if (menuItem.getItemId() == R.id.manually_verify) { - showManuallyVerifyDialog(); - return true; - } else { - return super.onOptionsItemSelected(menuItem); - } - } - private void showManuallyVerifyDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.manually_verify); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index 5fba1664..69dd47e7 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -71,6 +71,7 @@ import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.utils.ExceptionHelper; +import eu.siacs.conversations.xmpp.OnUpdateBlocklist; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -94,6 +95,7 @@ public abstract class XmppActivity extends Activity { protected boolean mUseSubject = true; private DisplayMetrics metrics; + protected int mTheme; protected interface OnValueEdited { public void onValueEdited(String value); @@ -198,7 +200,7 @@ public abstract class XmppActivity extends Activity { xmppConnectionServiceBound = false; } stopService(new Intent(XmppActivity.this, - XmppConnectionService.class)); + XmppConnectionService.class)); finish(); } }); @@ -208,13 +210,13 @@ public abstract class XmppActivity extends Activity { @Override public void onClick(DialogInterface dialog, int which) { Uri uri = Uri - .parse("market://details?id=org.sufficientlysecure.keychain"); + .parse("market://details?id=org.sufficientlysecure.keychain"); Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri); PackageManager manager = getApplicationContext() - .getPackageManager(); + .getPackageManager(); List<ResolveInfo> infos = manager - .queryIntentActivities(marketIntent, 0); + .queryIntentActivities(marketIntent, 0); if (infos.size() > 0) { startActivity(marketIntent); } else { @@ -244,6 +246,9 @@ public abstract class XmppActivity extends Activity { if (this instanceof XmppConnectionService.OnMucRosterUpdate) { this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this); } + if (this instanceof OnUpdateBlocklist) { + this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this); + } } protected void unregisterListeners() { @@ -259,9 +264,13 @@ public abstract class XmppActivity extends Activity { if (this instanceof XmppConnectionService.OnMucRosterUpdate) { this.xmppConnectionService.removeOnMucRosterUpdateListener(); } + if (this instanceof OnUpdateBlocklist) { + this.xmppConnectionService.removeOnUpdateBlocklistListener(); + } } - public boolean onOptionsItemSelected(MenuItem item) { + @Override + public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { case R.id.action_settings: startActivity(new Intent(this, SettingsActivity.class)); @@ -292,15 +301,14 @@ public abstract class XmppActivity extends Activity { mPrimaryColor = getResources().getColor(R.color.primary); mSecondaryBackgroundColor = getResources().getColor( R.color.secondarybackground); - if (getPreferences().getBoolean("use_larger_font", false)) { - setTheme(R.style.ConversationsTheme_LargerText); - } + this.mTheme = findTheme(); + setTheme(this.mTheme); mUseSubject = getPreferences().getBoolean("use_subject", true); } protected SharedPreferences getPreferences() { return PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()); + .getDefaultSharedPreferences(getApplicationContext()); } public boolean useSubjectToIdentifyConference() { @@ -312,7 +320,7 @@ public abstract class XmppActivity extends Activity { } public void switchToConversation(Conversation conversation, String text, - boolean newTask) { + boolean newTask) { switchToConversation(conversation,text,null,newTask); } @@ -372,7 +380,7 @@ public abstract class XmppActivity extends Activity { @Override public void userInputRequried(PendingIntent pi, - Account account) { + Account account) { try { startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0); @@ -383,15 +391,15 @@ public abstract class XmppActivity extends Activity { @Override public void success(Account account) { xmppConnectionService.databaseBackend - .updateAccount(account); + .updateAccount(account); xmppConnectionService.sendPresencePacket(account, xmppConnectionService.getPresenceGenerator() - .sendPresence(account)); + .sendPresence(account)); if (conversation != null) { conversation - .setNextEncryption(Message.ENCRYPTION_PGP); + .setNextEncryption(Message.ENCRYPTION_PGP); xmppConnectionService.databaseBackend - .updateConversation(conversation); + .updateConversation(conversation); } } @@ -420,7 +428,7 @@ public abstract class XmppActivity extends Activity { } protected void showAddToRosterDialog(final Conversation conversation) { - final Jid jid = conversation.getContactJid(); + final Jid jid = conversation.getJid(); AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(jid.toString()); builder.setMessage(getString(R.string.not_in_roster)); @@ -430,7 +438,7 @@ public abstract class XmppActivity extends Activity { @Override public void onClick(DialogInterface dialog, int which) { - final Jid jid = conversation.getContactJid(); + final Jid jid = conversation.getJid(); Account account = conversation.getAccount(); Contact contact = account.getRoster().getContact(jid); xmppConnectionService.createContact(contact); @@ -462,7 +470,7 @@ public abstract class XmppActivity extends Activity { } private void warnMutalPresenceSubscription(final Conversation conversation, - final OnPresenceSelected listener) { + final OnPresenceSelected listener) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(conversation.getContact().getJid().toString()); builder.setMessage(R.string.without_mutual_presence_updates); @@ -485,13 +493,13 @@ public abstract class XmppActivity extends Activity { } protected void quickPasswordEdit(String previousValue, - OnValueEdited callback) { + OnValueEdited callback) { quickEdit(previousValue, callback, true); } @SuppressLint("InflateParams") private void quickEdit(final String previousValue, - final OnValueEdited callback, boolean password) { + final OnValueEdited callback, boolean password) { AlertDialog.Builder builder = new AlertDialog.Builder(this); View view = getLayoutInflater().inflate(R.layout.quickedit, null); final EditText editor = (EditText) view.findViewById(R.id.editor); @@ -521,7 +529,7 @@ public abstract class XmppActivity extends Activity { } public void selectPresence(final Conversation conversation, - final OnPresenceSelected listener) { + final OnPresenceSelected listener) { final Contact contact = conversation.getContact(); if (conversation.hasValidOtrSession()) { SessionID id = conversation.getOtrSession().getSessionID(); @@ -576,7 +584,7 @@ public abstract class XmppActivity extends Activity { @Override public void onClick(DialogInterface dialog, - int which) { + int which) { presence.delete(0, presence.length()); presence.append(presencesArray[which]); } @@ -600,7 +608,7 @@ public abstract class XmppActivity extends Activity { } protected void onActivityResult(int requestCode, int resultCode, - final Intent data) { + final Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) { @@ -608,19 +616,19 @@ public abstract class XmppActivity extends Activity { Jid jid = Jid.fromString(data.getStringExtra("contact")); String conversationUuid = data.getStringExtra("conversation"); Conversation conversation = xmppConnectionService - .findConversationByUuid(conversationUuid); + .findConversationByUuid(conversationUuid); if (conversation.getMode() == Conversation.MODE_MULTI) { xmppConnectionService.invite(conversation, jid); } else { List<Jid> jids = new ArrayList<Jid>(); - jids.add(conversation.getContactJid().toBareJid()); + jids.add(conversation.getJid().toBareJid()); jids.add(jid); xmppConnectionService.createAdhocConference(conversation.getAccount(), jids, adhocCallback); } } catch (final InvalidJidException ignored) { } - } + } } private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() { @@ -688,18 +696,18 @@ public abstract class XmppActivity extends Activity { } protected void registerNdefPushMessageCallback() { - NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); - if (nfcAdapter != null && nfcAdapter.isEnabled()) { - nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { - @Override - public NdefMessage createNdefMessage(NfcEvent nfcEvent) { - return new NdefMessage(new NdefRecord[]{ - NdefRecord.createUri(getShareableUri()), - NdefRecord.createApplicationRecord("eu.siacs.conversations") - }); - } - }, this); - } + NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this); + if (nfcAdapter != null && nfcAdapter.isEnabled()) { + nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() { + @Override + public NdefMessage createNdefMessage(NfcEvent nfcEvent) { + return new NdefMessage(new NdefRecord[]{ + NdefRecord.createUri(getShareableUri()), + NdefRecord.createApplicationRecord("eu.siacs.conversations") + }); + } + }, this); + } } protected void unregisterNdefPushMessageCallback() { @@ -721,6 +729,14 @@ public abstract class XmppActivity extends Activity { } } + protected int findTheme() { + if (getPreferences().getBoolean("use_larger_font", false)) { + return R.style.ConversationsTheme_LargerText; + } else { + return R.style.ConversationsTheme; + } + } + @Override public void onPause() { super.onPause(); @@ -823,13 +839,13 @@ public abstract class XmppActivity extends Activity { try { task.execute(message); } catch (final RejectedExecutionException ignored) { - } + } } } } public static boolean cancelPotentialWork(Message message, - ImageView imageView) { + ImageView imageView) { final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView); if (bitmapWorkerTask != null) { @@ -858,7 +874,7 @@ public abstract class XmppActivity extends Activity { private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; public AsyncDrawable(Resources res, Bitmap bitmap, - BitmapWorkerTask bitmapWorkerTask) { + BitmapWorkerTask bitmapWorkerTask) { super(res, bitmap); bitmapWorkerTaskReference = new WeakReference<>( bitmapWorkerTask); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java index c5ee9ba6..32f95431 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ConversationAdapter.java @@ -2,7 +2,6 @@ package eu.siacs.conversations.ui.adapter; import java.util.List; -import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Downloadable; @@ -35,7 +34,7 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { public View getView(int position, View view, ViewGroup parent) { if (view == null) { LayoutInflater inflater = (LayoutInflater) activity - .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); view = inflater.inflate(R.layout.conversation_list_row, parent, false); } @@ -54,19 +53,19 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { } } TextView convName = (TextView) view - .findViewById(R.id.conversation_name); + .findViewById(R.id.conversation_name); if (conversation.getMode() == Conversation.MODE_SINGLE || activity.useSubjectToIdentifyConference()) { convName.setText(conversation.getName()); } else { - convName.setText(conversation.getContactJid().toBareJid().toString()); + convName.setText(conversation.getJid().toBareJid().toString()); } TextView mLastMessage = (TextView) view - .findViewById(R.id.conversation_lastmsg); + .findViewById(R.id.conversation_lastmsg); TextView mTimestamp = (TextView) view - .findViewById(R.id.conversation_lastupdate); + .findViewById(R.id.conversation_lastupdate); ImageView imagePreview = (ImageView) view - .findViewById(R.id.conversation_lastimage); + .findViewById(R.id.conversation_lastimage); Message message = conversation.getLatestMessage(); @@ -154,12 +153,12 @@ public class ConversationAdapter extends ArrayAdapter<Conversation> { imagePreview.setVisibility(View.GONE); } mTimestamp.setText(UIHelper.readableTimeDifference(getContext(), - conversation.getLatestMessage().getTimeSent())); + conversation.getLatestMessage().getTimeSent())); ImageView profilePicture = (ImageView) view - .findViewById(R.id.conversation_image); + .findViewById(R.id.conversation_image); profilePicture.setImageBitmap(activity.avatarService().get( - conversation, activity.getPixel(56))); + conversation, activity.getPixel(56))); return view; } diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java index 0865d1aa..91fb021c 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/ListItemAdapter.java @@ -2,6 +2,7 @@ package eu.siacs.conversations.ui.adapter; import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.ListItem; import eu.siacs.conversations.ui.XmppActivity; @@ -10,6 +11,7 @@ import eu.siacs.conversations.xmpp.jid.Jid; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -22,6 +24,17 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { protected XmppActivity activity; protected boolean showDynamicTags = false; + private View.OnClickListener onTagTvClick = new View.OnClickListener() { + @Override + public void onClick(View view) { + if (view instanceof TextView && mOnTagClickedListener != null) { + TextView tv = (TextView) view; + final String tag = tv.getText().toString(); + mOnTagClickedListener.onTagClicked(tag); + } + } + }; + private OnTagClickedListener mOnTagClickedListener = null; public ListItemAdapter(XmppActivity activity, List<ListItem> objects) { super(activity, 0, objects); @@ -53,6 +66,7 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tagLayout,false); tv.setText(tag.getName()); tv.setBackgroundColor(tag.getColor()); + tv.setOnClickListener(this.onTagTvClick); tagLayout.addView(tv); } } @@ -68,4 +82,12 @@ public class ListItemAdapter extends ArrayAdapter<ListItem> { return view; } + public void setOnTagClickedListener(OnTagClickedListener listener) { + this.mOnTagClickedListener = listener; + } + + public interface OnTagClickedListener { + public void onTagClicked(String tag); + } + } 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 00000e2b..83b4e41b 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -23,7 +23,6 @@ import android.widget.Toast; import java.util.List; -import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -271,7 +270,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { @Override public void onClick(View v) { - startDonwloadable(message); + startDownloadable(message); } }); viewHolder.download_button.setOnLongClickListener(openContextMenu); @@ -287,7 +286,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { @Override public void onClick(View v) { - openDonwloadable(file); + openDownloadable(file); } }); viewHolder.download_button.setOnLongClickListener(openContextMenu); @@ -441,6 +440,8 @@ public class MessageAdapter extends ArrayAdapter<Message> { } view.setLayoutParams(view.getLayoutParams()); return view; + } else if (viewHolder.messageBody == null || viewHolder.image == null) { + return view; //avoiding weird platform bugs } else if (type == RECEIVED) { Contact contact = message.getContact(); if (contact != null) { @@ -449,38 +450,36 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(getDisplayedMucCounterpart(message.getCounterpart()), activity.getPixel(48))); } - } else if (type == SENT && viewHolder.contact_picture != null) { + } else if (type == SENT) { viewHolder.contact_picture.setImageBitmap(activity.avatarService().get(account, activity.getPixel(48))); } - if (viewHolder != null && viewHolder.contact_picture != null) { - viewHolder.contact_picture - .setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - if (MessageAdapter.this.mOnContactPictureClickedListener != null) { - MessageAdapter.this.mOnContactPictureClickedListener - .onContactPictureClicked(message); - } + viewHolder.contact_picture + .setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (MessageAdapter.this.mOnContactPictureClickedListener != null) { + MessageAdapter.this.mOnContactPictureClickedListener + .onContactPictureClicked(message); } - }); - viewHolder.contact_picture - .setOnLongClickListener(new OnLongClickListener() { - - @Override - public boolean onLongClick(View v) { - if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { - MessageAdapter.this.mOnContactPictureLongClickedListener - .onContactPictureLongClicked(message); - return true; - } else { - return false; - } + + } + }); + viewHolder.contact_picture + .setOnLongClickListener(new OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + if (MessageAdapter.this.mOnContactPictureLongClickedListener != null) { + MessageAdapter.this.mOnContactPictureLongClickedListener + .onContactPictureLongClicked(message); + return true; + } else { + return false; } - }); - } + } + }); if (message.getDownloadable() != null && message.getDownloadable().getStatus() != Downloadable.STATUS_UPLOADING) { Downloadable d = message.getDownloadable(); @@ -549,7 +548,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { return view; } - public void startDonwloadable(Message message) { + public void startDownloadable(Message message) { Downloadable downloadable = message.getDownloadable(); if (downloadable != null) { if (!downloadable.start()) { @@ -559,7 +558,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { } } - public void openDonwloadable(DownloadableFile file) { + public void openDownloadable(DownloadableFile file) { if (!file.exists()) { Toast.makeText(activity,R.string.file_deleted,Toast.LENGTH_SHORT).show(); return; diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index 8c1a8dea..a09b4d0f 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -140,15 +140,18 @@ public class DNSHelper { } ArrayList<Bundle> values = new ArrayList<>(); for (SRV srv : result) { - Bundle namePort = new Bundle(); - namePort.putString("name", srv.getName()); - namePort.putInt("port", srv.getPort()); + boolean added = false; + if (ips6.containsKey(srv.getName())) { + values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips6)); + added = true; + } if (ips4.containsKey(srv.getName())) { - ArrayList<String> ip = ips4.get(srv.getName()); - Collections.shuffle(ip, rnd); - namePort.putString("ipv4", ip.get(0)); + values.add(createNamePortBundle(srv.getName(),srv.getPort(),ips4)); + added = true; + } + if (!added) { + values.add(createNamePortBundle(srv.getName(),srv.getPort(),null)); } - values.add(namePort); } bundle.putParcelableArrayList("values", values); } catch (SocketTimeoutException e) { @@ -159,6 +162,18 @@ public class DNSHelper { return bundle; } + private static Bundle createNamePortBundle(String name, int port, TreeMap<String, ArrayList<String>> ips) { + Bundle namePort = new Bundle(); + namePort.putString("name", name); + namePort.putInt("port", port); + if (ips!=null) { + ArrayList<String> ip = ips.get(name); + Collections.shuffle(ip, new Random()); + namePort.putString("ip", ip.get(0)); + } + return namePort; + } + final protected static char[] hexArray = "0123456789ABCDEF".toCharArray(); public static String bytesToHex(byte[] bytes) { diff --git a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java index 87973159..9a5cbaaf 100644 --- a/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/PhoneHelper.java @@ -16,10 +16,7 @@ import android.provider.ContactsContract.Profile; public class PhoneHelper { - public static void loadPhoneContacts(Context context, - final OnPhoneContactsLoadedListener listener) { - final List<Bundle> phoneContacts = new ArrayList<Bundle>(); - + public static void loadPhoneContacts(Context context,final List<Bundle> phoneContacts, final OnPhoneContactsLoadedListener listener) { final String[] PROJECTION = new String[] { ContactsContract.Data._ID, ContactsContract.Data.DISPLAY_NAME, ContactsContract.Data.PHOTO_URI, diff --git a/src/main/java/eu/siacs/conversations/utils/Validator.java b/src/main/java/eu/siacs/conversations/utils/Validator.java deleted file mode 100644 index 00130fa2..00000000 --- a/src/main/java/eu/siacs/conversations/utils/Validator.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.siacs.conversations.utils; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Validator { - public static final Pattern VALID_JID = Pattern.compile( - "^[^@/<>'\"\\s]+@[^@/<>'\"\\s]+$", Pattern.CASE_INSENSITIVE); - - public static boolean isValidJid(String jid) { - Matcher matcher = VALID_JID.matcher(jid); - return matcher.find(); - } -} diff --git a/src/main/java/eu/siacs/conversations/utils/Xmlns.java b/src/main/java/eu/siacs/conversations/utils/Xmlns.java new file mode 100644 index 00000000..67de7c79 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/utils/Xmlns.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.utils; + +public final class Xmlns { + public static final String BLOCKING = "urn:xmpp:blocking"; + public static final String ROSTER = "jabber:iq:roster"; + public static final String REGISTER = "jabber:iq:register"; +} diff --git a/src/main/java/eu/siacs/conversations/utils/XmppUri.java b/src/main/java/eu/siacs/conversations/utils/XmppUri.java index a9b8d1c0..aacb6362 100644 --- a/src/main/java/eu/siacs/conversations/utils/XmppUri.java +++ b/src/main/java/eu/siacs/conversations/utils/XmppUri.java @@ -5,7 +5,6 @@ import android.net.Uri; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -54,7 +53,7 @@ public class XmppUri { final String NEEDLE = "otr-fingerprint="; int index = query.indexOf(NEEDLE); if (index >= 0 && query.length() >= (NEEDLE.length() + index + 40)) { - return CryptoHelper.prettifyFingerprint(query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40)); + return query.substring(index + NEEDLE.length(), index + NEEDLE.length() + 40); } else { return null; } diff --git a/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java deleted file mode 100644 index b777c10c..00000000 --- a/src/main/java/eu/siacs/conversations/utils/zlib/ZLibInputStream.java +++ /dev/null @@ -1,54 +0,0 @@ -package eu.siacs.conversations.utils.zlib; - -import java.io.IOException; -import java.io.InputStream; -import java.util.zip.Inflater; -import java.util.zip.InflaterInputStream; - -/** - * ZLibInputStream is a zlib and input stream compatible version of an - * InflaterInputStream. This class solves the incompatibility between - * {@link InputStream#available()} and {@link InflaterInputStream#available()}. - */ -public class ZLibInputStream extends InflaterInputStream { - - /** - * Construct a ZLibInputStream, reading data from the underlying stream. - * - * @param is - * The {@code InputStream} to read data from. - * @throws IOException - * If an {@code IOException} occurs. - */ - public ZLibInputStream(InputStream is) throws IOException { - super(is, new Inflater(), 512); - } - - /** - * Provide a more InputStream compatible version of available. A return - * value of 1 means that it is likly to read one byte without blocking, 0 - * means that the system is known to block for more input. - * - * @return 0 if no data is available, 1 otherwise - * @throws IOException - */ - @Override - public int available() throws IOException { - /* - * This is one of the funny code blocks. InflaterInputStream.available - * violates the contract of InputStream.available, which breaks kXML2. - * - * I'm not sure who's to blame, oracle/sun for a broken api or the - * google guys for mixing a sun bug with a xml reader that can't handle - * it.... - * - * Anyway, this simple if breaks suns distorted reality, but helps to - * use the api as intended. - */ - if (inf.needsInput()) { - return 0; - } - return super.available(); - } - -} diff --git a/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java b/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java deleted file mode 100644 index 8b3f5e68..00000000 --- a/src/main/java/eu/siacs/conversations/utils/zlib/ZLibOutputStream.java +++ /dev/null @@ -1,95 +0,0 @@ -package eu.siacs.conversations.utils.zlib; - -import java.io.IOException; -import java.io.OutputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.security.NoSuchAlgorithmException; -import java.util.zip.Deflater; -import java.util.zip.DeflaterOutputStream; - -/** - * <p> - * Android 2.2 includes Java7 FLUSH_SYNC option, which will be used by this - * Implementation, preferable via reflection. The @hide was remove in API level - * 19. This class might thus go away in the future. - * </p> - * <p> - * Please use {@link ZLibOutputStream#SUPPORTED} to check for flush - * compatibility. - * </p> - */ -public class ZLibOutputStream extends DeflaterOutputStream { - - /** - * The reflection based flush method. - */ - - private final static Method method; - /** - * SUPPORTED is true if a flush compatible method exists. - */ - public final static boolean SUPPORTED; - - /** - * Static block to initialize {@link #SUPPORTED} and {@link #method}. - */ - static { - Method m = null; - try { - m = Deflater.class.getMethod("deflate", byte[].class, int.class, - int.class, int.class); - } catch (SecurityException e) { - } catch (NoSuchMethodException e) { - } - method = m; - SUPPORTED = (method != null); - } - - /** - * Create a new ZLib compatible output stream wrapping the given low level - * stream. ZLib compatiblity means we will send a zlib header. - * - * @param os - * OutputStream The underlying stream. - * @throws IOException - * In case of a lowlevel transfer problem. - * @throws NoSuchAlgorithmException - * In case of a {@link Deflater} error. - */ - public ZLibOutputStream(OutputStream os) throws IOException, - NoSuchAlgorithmException { - super(os, new Deflater(Deflater.BEST_COMPRESSION)); - } - - /** - * Flush the given stream, preferring Java7 FLUSH_SYNC if available. - * - * @throws IOException - * In case of a lowlevel exception. - */ - @Override - public void flush() throws IOException { - if (!SUPPORTED) { - super.flush(); - return; - } - try { - int count = 0; - do { - count = (Integer) method.invoke(def, buf, 0, buf.length, 3); - if (count > 0) { - out.write(buf, 0, count); - } - } while (count > 0); - } catch (IllegalArgumentException e) { - throw new IOException("Can't flush"); - } catch (IllegalAccessException e) { - throw new IOException("Can't flush"); - } catch (InvocationTargetException e) { - throw new IOException("Can't flush"); - } - super.flush(); - } - -} diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 02c3e695..51708759 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,9 +1,12 @@ package eu.siacs.conversations.xml; +import android.util.Log; + import java.util.ArrayList; import java.util.Hashtable; import java.util.List; +import eu.siacs.conversations.Config; import eu.siacs.conversations.utils.XmlHelper; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -59,16 +62,16 @@ public class Element { if (child.getName().equals(name) && (child.getAttribute("xmlns").equals(xmlns))) { return child; - } + } } return null; } - public boolean hasChild(String name) { + public boolean hasChild(final String name) { return findChild(name) != null; } - public boolean hasChild(String name, String xmlns) { + public boolean hasChild(final String name, final String xmlns) { return findChild(name, xmlns) != null; } @@ -107,14 +110,15 @@ public class Element { public Jid getAttributeAsJid(String name) { final String jid = this.getAttribute(name); - if (jid != null && !jid.isEmpty()) { - try { - return Jid.fromString(jid); - } catch (final InvalidJidException e) { - return null; - } - } - return null; + if (jid != null && !jid.isEmpty()) { + try { + return Jid.fromString(jid); + } catch (final InvalidJidException e) { + Log.e(Config.LOGTAG, "could not parse jid " + jid); + return null; + } + } + return null; } public Hashtable<String, String> getAttributes() { @@ -159,4 +163,9 @@ public class Element { public void setAttribute(String name, int value) { this.setAttribute(name, Integer.toString(value)); } + + public boolean getAttributeAsBoolean(String name) { + String attr = getAttribute(name); + return (attr != null && (attr.equalsIgnoreCase("true") || attr.equalsIgnoreCase("1"))); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java new file mode 100644 index 00000000..e45eba73 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnAdvancedStreamFeaturesLoaded.java @@ -0,0 +1,7 @@ +package eu.siacs.conversations.xmpp; + +import eu.siacs.conversations.entities.Account; + +public interface OnAdvancedStreamFeaturesLoaded { + public void onAdvancedStreamFeaturesAvailable(final Account account); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java index 849e8e76..20b17f02 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java +++ b/src/main/java/eu/siacs/conversations/xmpp/OnContactStatusChanged.java @@ -3,5 +3,5 @@ package eu.siacs.conversations.xmpp; import eu.siacs.conversations.entities.Contact; public interface OnContactStatusChanged { - public void onContactStatusChanged(Contact contact, boolean online); + public void onContactStatusChanged(final Contact contact, final boolean online); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java b/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java new file mode 100644 index 00000000..92e72cfa --- /dev/null +++ b/src/main/java/eu/siacs/conversations/xmpp/OnUpdateBlocklist.java @@ -0,0 +1,13 @@ +package eu.siacs.conversations.xmpp; + +public interface OnUpdateBlocklist { + // Use an enum instead of a boolean to make sure we don't run into the boolean trap + // (`onUpdateBlocklist(true)' doesn't read well, and could be confusing). + public static enum Status { + BLOCKED, + UNBLOCKED + } + + @SuppressWarnings("MethodNameSameAsClassName") + public void OnUpdateBlocklist(final Status status); +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index fb151427..f7f0c346 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -30,10 +30,12 @@ import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import javax.net.ssl.HostnameVerifier; @@ -48,10 +50,10 @@ import eu.siacs.conversations.crypto.sasl.Plain; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.ScramSha1; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.DNSHelper; -import eu.siacs.conversations.utils.zlib.ZLibInputStream; -import eu.siacs.conversations.utils.zlib.ZLibOutputStream; +import eu.siacs.conversations.utils.Xmlns; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Tag; import eu.siacs.conversations.xml.TagWriter; @@ -78,21 +80,20 @@ public class XmppConnection implements Runnable { private static final int PACKET_PRESENCE = 2; private final Context applicationContext; protected Account account; - private WakeLock wakeLock; + private final WakeLock wakeLock; private Socket socket; private XmlReader tagReader; private TagWriter tagWriter; - private Features features = new Features(this); + private final Features features = new Features(this); private boolean shouldBind = true; private boolean shouldAuthenticate = true; private Element streamFeatures; - private HashMap<String, List<String>> disco = new HashMap<>(); + private final HashMap<String, List<String>> disco = new HashMap<>(); private String streamId = null; private int smVersion = 3; - private SparseArray<String> messageReceipts = new SparseArray<>(); + private final SparseArray<String> messageReceipts = new SparseArray<>(); - private boolean enabledCompression = false; private boolean enabledEncryption = false; private boolean enabledCarbons = false; @@ -103,19 +104,20 @@ public class XmppConnection implements Runnable { private long lastConnect = 0; private long lastSessionStarted = 0; private int attempt = 0; - private Hashtable<String, PacketReceived> packetCallbacks = new Hashtable<>(); + private final Map<String, PacketReceived> packetCallbacks = new Hashtable<>(); private OnPresencePacketReceived presenceListener = null; private OnJinglePacketReceived jingleListener = null; private OnIqPacketReceived unregisteredIqListener = null; private OnMessagePacketReceived messageListener = null; private OnStatusChanged statusListener = null; private OnBindListener bindListener = null; + private final ArrayList<OnAdvancedStreamFeaturesLoaded> advancedStreamFeaturesLoadedListeners = new ArrayList<>(); private OnMessageAcknowledged acknowledgedListener = null; private XmppConnectionService mXmppConnectionService = null; private SaslMechanism saslMechanism; - public XmppConnection(Account account, XmppConnectionService service) { + public XmppConnection(final Account account, final XmppConnectionService service) { this.account = account; this.wakeLock = service.getPowerManager().newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, account.getJid().toBareJid().toString()); @@ -131,7 +133,7 @@ public class XmppConnection implements Runnable { && (account.getStatus() != Account.State.ONLINE) && (account.getStatus() != Account.State.DISABLED)) { return; - } + } if (nextStatus == Account.State.ONLINE) { this.attempt = 0; } @@ -144,7 +146,6 @@ public class XmppConnection implements Runnable { protected void connect() { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); - enabledCompression = false; enabledEncryption = false; lastConnect = SystemClock.elapsedRealtime(); lastPingSent = SystemClock.elapsedRealtime(); @@ -156,15 +157,15 @@ public class XmppConnection implements Runnable { tagWriter = new TagWriter(); packetCallbacks.clear(); this.changeStatus(Account.State.CONNECTING); - Bundle result = DNSHelper.getSRVRecord(account.getServer()); - ArrayList<Parcelable> values = result.getParcelableArrayList("values"); + final Bundle result = DNSHelper.getSRVRecord(account.getServer()); + final ArrayList<Parcelable> 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) { - Bundle namePort = (Bundle) values.get(i); + final Bundle namePort = (Bundle) values.get(i); try { String srvRecordServer; try { @@ -173,9 +174,9 @@ public class XmppConnection implements Runnable { // TODO: Handle me?` srvRecordServer = ""; } - int srvRecordPort = namePort.getInt("port"); - String srvIpServer = namePort.getString("ipv4"); - InetSocketAddress addr; + 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() @@ -190,10 +191,10 @@ public class XmppConnection implements Runnable { socket = new Socket(); socket.connect(addr, 20000); socketError = false; - } catch (UnknownHostException e) { + } catch (final UnknownHostException e) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); i++; - } catch (IOException e) { + } catch (final IOException e) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); i++; } @@ -207,9 +208,9 @@ public class XmppConnection implements Runnable { } else { throw new IOException("timeout in dns"); } - OutputStream out = socket.getOutputStream(); + final OutputStream out = socket.getOutputStream(); tagWriter.setOutputStream(out); - InputStream in = socket.getInputStream(); + final InputStream in = socket.getInputStream(); tagReader.setInputStream(in); tagWriter.beginDocument(); sendStartStream(); @@ -225,14 +226,9 @@ public class XmppConnection implements Runnable { if (socket.isConnected()) { socket.close(); } - } catch (UnknownHostException e) { + } catch (final UnknownHostException | ConnectException e) { this.changeStatus(Account.State.SERVER_NOT_FOUND); - } catch (final ConnectException e) { - this.changeStatus(Account.State.SERVER_NOT_FOUND); - } catch (final IOException | XmlPullParserException e) { - Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); - this.changeStatus(Account.State.OFFLINE); - } catch (NoSuchAlgorithmException e) { + } catch (final IOException | XmlPullParserException | NoSuchAlgorithmException e) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": " + e.getMessage()); this.changeStatus(Account.State.OFFLINE); } finally { @@ -261,8 +257,6 @@ public class XmppConnection implements Runnable { processStreamFeatures(nextTag); } else if (nextTag.isStart("proceed")) { switchOverToTls(nextTag); - } else if (nextTag.isStart("compressed")) { - switchOverToZLib(nextTag); } else if (nextTag.isStart("success")) { final String challenge = tagReader.readElement(nextTag).getContent(); try { @@ -273,7 +267,7 @@ public class XmppConnection implements Runnable { } Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": logged in"); account.setKey(Account.PINNED_MECHANISM_KEY, - String.valueOf(saslMechanism.getPriority())); + String.valueOf(saslMechanism.getPriority())); tagReader.reset(); sendStartStream(); processStream(tagReader.readTag()); @@ -294,7 +288,7 @@ public class XmppConnection implements Runnable { } tagWriter.writeElement(response); } else if (nextTag.isStart("enabled")) { - Element enabled = tagReader.readElement(nextTag); + final Element enabled = tagReader.readElement(nextTag); if ("true".equals(enabled.getAttribute("resume"))) { this.streamId = enabled.getAttribute("id"); Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() @@ -306,14 +300,14 @@ public class XmppConnection implements Runnable { } this.lastSessionStarted = SystemClock.elapsedRealtime(); this.stanzasReceived = 0; - RequestPacket r = new RequestPacket(smVersion); + final RequestPacket r = new RequestPacket(smVersion); tagWriter.writeStanzaAsync(r); } else if (nextTag.isStart("resumed")) { lastPaketReceived = SystemClock.elapsedRealtime(); - Element resumed = tagReader.readElement(nextTag); - String h = resumed.getAttribute("h"); + final Element resumed = tagReader.readElement(nextTag); + final String h = resumed.getAttribute("h"); try { - int serverCount = Integer.parseInt(h); + final int serverCount = Integer.parseInt(h); if (serverCount != stanzasSent) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": session resumed with lost packages"); @@ -332,20 +326,19 @@ public class XmppConnection implements Runnable { } messageReceipts.clear(); } catch (final NumberFormatException ignored) { - } sendServiceDiscoveryInfo(account.getServer()); sendServiceDiscoveryItems(account.getServer()); sendInitialPing(); } else if (nextTag.isStart("r")) { tagReader.readElement(nextTag); - AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); + final AckPacket ack = new AckPacket(this.stanzasReceived, smVersion); tagWriter.writeStanzaAsync(ack); } else if (nextTag.isStart("a")) { - Element ack = tagReader.readElement(nextTag); + final Element ack = tagReader.readElement(nextTag); lastPaketReceived = SystemClock.elapsedRealtime(); - int serverSequence = Integer.parseInt(ack.getAttribute("h")); - String msgId = this.messageReceipts.get(serverSequence); + final int serverSequence = Integer.parseInt(ack.getAttribute("h")); + final String msgId = this.messageReceipts.get(serverSequence); if (msgId != null) { if (this.acknowledgedListener != null) { this.acknowledgedListener.onMessageAcknowledged( @@ -379,13 +372,12 @@ public class XmppConnection implements Runnable { private void sendInitialPing() { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": sending intial ping"); - IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + final IqPacket iq = new IqPacket(IqPacket.TYPE_GET); iq.setFrom(account.getJid()); iq.addChild("ping", "urn:xmpp:ping"); this.sendIqPacket(iq, new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { + public void onIqPacketReceived(final Account account, final IqPacket packet) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": online with resource " + account.getResource()); changeStatus(Account.State.ONLINE); @@ -393,7 +385,7 @@ public class XmppConnection implements Runnable { }); } - private Element processPacket(Tag currentTag, int packetType) + private Element processPacket(final Tag currentTag, final int packetType) throws XmlPullParserException, IOException { Element element; switch (packetType) { @@ -416,8 +408,8 @@ public class XmppConnection implements Runnable { } while (!nextTag.isEnd(element.getName())) { if (!nextTag.isNo()) { - Element child = tagReader.readElement(nextTag); - String type = currentTag.getAttribute("type"); + final Element child = tagReader.readElement(nextTag); + final String type = currentTag.getAttribute("type"); if (packetType == PACKET_IQ && "jingle".equals(child.getName()) && ("set".equalsIgnoreCase(type) || "get" @@ -437,9 +429,9 @@ public class XmppConnection implements Runnable { return element; } - private void processIq(Tag currentTag) throws XmlPullParserException, + private void processIq(final Tag currentTag) throws XmlPullParserException, IOException { - IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); + final IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ); if (packet.getId() == null) { return; // an iq packet without id is definitely invalid @@ -466,11 +458,11 @@ public class XmppConnection implements Runnable { } } - private void processMessage(Tag currentTag) throws XmlPullParserException, + private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException { - MessagePacket packet = (MessagePacket) processPacket(currentTag, + final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE); - String id = packet.getAttribute("id"); + final String id = packet.getAttribute("id"); if ((id != null) && (packetCallbacks.containsKey(id))) { if (packetCallbacks.get(id) instanceof OnMessagePacketReceived) { ((OnMessagePacketReceived) packetCallbacks.get(id)) @@ -482,11 +474,11 @@ public class XmppConnection implements Runnable { } } - private void processPresence(Tag currentTag) throws XmlPullParserException, + private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException { PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); - String id = packet.getAttribute("id"); + final String id = packet.getAttribute("id"); if ((id != null) && (packetCallbacks.containsKey(id))) { if (packetCallbacks.get(id) instanceof OnPresencePacketReceived) { ((OnPresencePacketReceived) packetCallbacks.get(id)) @@ -498,30 +490,8 @@ public class XmppConnection implements Runnable { } } - private void sendCompressionZlib() throws IOException { - Element compress = new Element("compress"); - compress.setAttribute("xmlns", "http://jabber.org/protocol/compress"); - compress.addChild("method").setContent("zlib"); - tagWriter.writeElement(compress); - } - - private void switchOverToZLib(final Tag currentTag) - throws XmlPullParserException, IOException, - NoSuchAlgorithmException { - tagReader.readTag(); // read tag close - tagWriter.setOutputStream(new ZLibOutputStream(tagWriter - .getOutputStream())); - tagReader - .setInputStream(new ZLibInputStream(tagReader.getInputStream())); - - sendStartStream(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": compression enabled"); - enabledCompression = true; - processStream(tagReader.readTag()); - } - private void sendStartTLS() throws IOException { - Tag startTLS = Tag.empty("starttls"); + final Tag startTLS = Tag.empty("starttls"); startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls"); tagWriter.writeTag(startTLS); } @@ -535,74 +505,61 @@ public class XmppConnection implements Runnable { return getPreferences().getBoolean("enable_legacy_ssl", false); } - private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, - IOException { - tagReader.readTag(); - try { - SSLContext sc = SSLContext.getInstance("TLS"); - sc.init(null, - new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()}, - mXmppConnectionService.getRNG()); - SSLSocketFactory factory = sc.getSocketFactory(); - - if (factory == null) { - throw new IOException("SSLSocketFactory was null"); - } - - final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); + private void switchOverToTls(final Tag currentTag) throws XmlPullParserException, IOException { + tagReader.readTag(); + try { + final SSLContext sc = SSLContext.getInstance("TLS"); + sc.init(null,new X509TrustManager[]{this.mXmppConnectionService.getMemorizingTrustManager()},mXmppConnectionService.getRNG()); + final SSLSocketFactory factory = sc.getSocketFactory(); + final HostnameVerifier verifier = this.mXmppConnectionService.getMemorizingTrustManager().wrapHostnameVerifier(new StrictHostnameVerifier()); + final InetAddress address = socket == null ? null : socket.getInetAddress(); + + if (factory == null || address == null || verifier == null) { + throw new IOException("could not setup ssl"); + } - if (socket == null || socket.isClosed()) { - throw new IOException("socket null or closed"); - } - final InetAddress address = socket.getInetAddress(); - if (address == null) { - throw new IOException("socket address was null"); - } + final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true); - final SSLSocket sslSocket = (SSLSocket) factory.createSocket(socket,address.getHostAddress(), socket.getPort(),true); + if (sslSocket == null) { + throw new IOException("could not initialize ssl socket"); + } - // Support all protocols except legacy SSL. - // The min SDK version prevents us having to worry about SSLv2. In - // future, this may be true of SSLv3 as well. - final String[] supportProtocols; - if (enableLegacySSL()) { - supportProtocols = sslSocket.getSupportedProtocols(); - } else { - final List<String> supportedProtocols = new LinkedList<>( - Arrays.asList(sslSocket.getSupportedProtocols())); - supportedProtocols.remove("SSLv3"); - supportProtocols = new String[supportedProtocols.size()]; - supportedProtocols.toArray(supportProtocols); - } - sslSocket.setEnabledProtocols(supportProtocols); - - if (verifier != null - && !verifier.verify(account.getServer().getDomainpart(), - sslSocket.getSession())) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); - disconnect(true); - changeStatus(Account.State.SECURITY_ERROR); - } - tagReader.setInputStream(sslSocket.getInputStream()); - tagWriter.setOutputStream(sslSocket.getOutputStream()); - sendStartStream(); - Log.d(Config.LOGTAG, account.getJid().toBareJid() - + ": TLS connection established"); - enabledEncryption = true; - processStream(tagReader.readTag()); - sslSocket.close(); - } catch (final NoSuchAlgorithmException | KeyManagementException e1) { - e1.printStackTrace(); - } + final String[] supportProtocols; + if (enableLegacySSL()) { + supportProtocols = sslSocket.getSupportedProtocols(); + } else { + final Collection<String> supportedProtocols = new LinkedList<>( + Arrays.asList(sslSocket.getSupportedProtocols())); + supportedProtocols.remove("SSLv3"); + supportProtocols = new String[supportedProtocols.size()]; + supportedProtocols.toArray(supportProtocols); + } + sslSocket.setEnabledProtocols(supportProtocols); + + if (!verifier.verify(account.getServer().getDomainpart(),sslSocket.getSession())) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } + tagReader.setInputStream(sslSocket.getInputStream()); + tagWriter.setOutputStream(sslSocket.getOutputStream()); + sendStartStream(); + Log.d(Config.LOGTAG, account.getJid().toBareJid()+ ": TLS connection established"); + enabledEncryption = true; + processStream(tagReader.readTag()); + sslSocket.close(); + } catch (final NoSuchAlgorithmException | KeyManagementException e1) { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": TLS certificate verification failed"); + disconnect(true); + changeStatus(Account.State.SECURITY_ERROR); + } } - private void processStreamFeatures(Tag currentTag) + private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException { this.streamFeatures = tagReader.readElement(currentTag); if (this.streamFeatures.hasChild("starttls") && !enabledEncryption) { sendStartTLS(); - } else if (compressionAvailable()) { - sendCompressionZlib(); } else if (this.streamFeatures.hasChild("register") && account.isOptionSet(Account.OPTION_REGISTER) && enabledEncryption) { @@ -619,10 +576,10 @@ public class XmppConnection implements Runnable { auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); if (mechanisms.contains("SCRAM-SHA-1")) { saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains("DIGEST-MD5")) { - saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains("PLAIN")) { saslMechanism = new Plain(tagWriter, account); + } else if (mechanisms.contains("DIGEST-MD5")) { + saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); } final JSONObject keys = account.getKeys(); try { @@ -634,7 +591,7 @@ public class XmppConnection implements Runnable { "). Possible downgrade attack?"); disconnect(true); changeStatus(Account.State.SECURITY_ERROR); - } + } } catch (final JSONException e) { Log.d(Config.LOGTAG, "Parse error while checking pinned auth mechanism"); } @@ -647,7 +604,7 @@ public class XmppConnection implements Runnable { } else if (this.streamFeatures.hasChild("sm", "urn:xmpp:sm:" + smVersion) && streamId != null) { - ResumePacket resume = new ResumePacket(this.streamId, + final ResumePacket resume = new ResumePacket(this.streamId, stanzasReceived, smVersion); this.tagWriter.writeStanzaAsync(resume); } else if (this.streamFeatures.hasChild("bind") && shouldBind) { @@ -658,67 +615,44 @@ public class XmppConnection implements Runnable { } } - private boolean compressionAvailable() { - if (!this.streamFeatures.hasChild("compression", - "http://jabber.org/features/compress")) - return false; - if (!ZLibOutputStream.SUPPORTED) - return false; - if (!account.isOptionSet(Account.OPTION_USECOMPRESSION)) - return false; - - Element compression = this.streamFeatures.findChild("compression", - "http://jabber.org/features/compress"); - for (Element child : compression.getChildren()) { - if (!"method".equals(child.getName())) - continue; - - if ("zlib".equalsIgnoreCase(child.getContent())) { - return true; - } - } - return false; - } - - private List<String> extractMechanisms(Element stream) { - ArrayList<String> mechanisms = new ArrayList<>(stream + private List<String> extractMechanisms(final Element stream) { + final ArrayList<String> mechanisms = new ArrayList<>(stream .getChildren().size()); - for (Element child : stream.getChildren()) { + for (final Element child : stream.getChildren()) { mechanisms.add(child.getContent()); } return mechanisms; } private void sendRegistryRequest() { - IqPacket register = new IqPacket(IqPacket.TYPE_GET); + final IqPacket register = new IqPacket(IqPacket.TYPE_GET); register.query("jabber:iq:register"); register.setTo(account.getServer()); sendIqPacket(register, new OnIqPacketReceived() { @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Element instructions = packet.query().findChild("instructions"); + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final Element instructions = packet.query().findChild("instructions"); if (packet.query().hasChild("username") && (packet.query().hasChild("password"))) { - IqPacket register = new IqPacket(IqPacket.TYPE_SET); - Element username = new Element("username") - .setContent(account.getUsername()); - Element password = new Element("password") - .setContent(account.getPassword()); + final IqPacket register = new IqPacket(IqPacket.TYPE_SET); + final Element username = new Element("username") + .setContent(account.getUsername()); + final Element password = new Element("password") + .setContent(account.getPassword()); register.query("jabber:iq:register").addChild(username); register.query().addChild(password); sendIqPacket(register, new OnIqPacketReceived() { @Override - public void onIqPacketReceived(Account account, - IqPacket packet) { + public void onIqPacketReceived(final Account account, final IqPacket packet) { if (packet.getType() == IqPacket.TYPE_RESULT) { account.setOption(Account.OPTION_REGISTER, false); changeStatus(Account.State.REGISTRATION_SUCCESSFUL); } else if (packet.hasChild("error") && (packet.findChild("error") - .hasChild("conflict"))) { + .hasChild("conflict"))) { changeStatus(Account.State.REGISTRATION_CONFLICT); } else { changeStatus(Account.State.REGISTRATION_FAILED); @@ -738,14 +672,14 @@ public class XmppConnection implements Runnable { }); } - private void sendBindRequest() throws IOException { - IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + private void sendBindRequest() { + final IqPacket iq = new IqPacket(IqPacket.TYPE_SET); iq.addChild("bind", "urn:ietf:params:xml:ns:xmpp-bind") .addChild("resource").setContent(account.getResource()); this.sendUnboundIqPacket(iq, new OnIqPacketReceived() { @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - Element bind = packet.findChild("bind"); + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final Element bind = packet.findChild("bind"); if (bind != null) { final Element jid = bind.findChild("jid"); if (jid != null && jid.getContent() != null) { @@ -756,14 +690,14 @@ public class XmppConnection implements Runnable { } if (streamFeatures.hasChild("sm", "urn:xmpp:sm:3")) { smVersion = 3; - EnablePacket enable = new EnablePacket(smVersion); + final EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; messageReceipts.clear(); } else if (streamFeatures.hasChild("sm", - "urn:xmpp:sm:2")) { + "urn:xmpp:sm:2")) { smVersion = 2; - EnablePacket enable = new EnablePacket(smVersion); + final EnablePacket enable = new EnablePacket(smVersion); tagWriter.writeStanzaAsync(enable); stanzasSent = 0; messageReceipts.clear(); @@ -787,7 +721,7 @@ public class XmppConnection implements Runnable { if (this.streamFeatures.hasChild("session")) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": sending deprecated session"); - IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); + final IqPacket startSession = new IqPacket(IqPacket.TYPE_SET); startSession.addChild("session", "urn:ietf:params:xml:ns:xmpp-session"); this.sendUnboundIqPacket(startSession, null); @@ -806,10 +740,10 @@ public class XmppConnection implements Runnable { this.sendIqPacket(iq, new OnIqPacketReceived() { @Override - public void onIqPacketReceived(Account account, IqPacket packet) { + public void onIqPacketReceived(final Account account, final IqPacket packet) { final List<Element> elements = packet.query().getChildren(); final List<String> features = new ArrayList<>(); - for (Element element : elements) { + for (final Element element : elements) { if (element.getName().equals("identity")) { if ("irc".equals(element.getAttribute("type"))) { //add fake feature to not confuse irc and real muc @@ -823,6 +757,9 @@ public class XmppConnection implements Runnable { if (account.getServer().equals(server.toDomainJid())) { enableAdvancedStreamFeatures(); + for (final OnAdvancedStreamFeaturesLoaded listener : advancedStreamFeaturesLoadedListeners) { + listener.onAdvancedStreamFeaturesAvailable(account); + } } } }); @@ -835,6 +772,10 @@ public class XmppConnection implements Runnable { sendEnableCarbons(); } } + if (getFeatures().blocking()) { + Log.d(Config.LOGTAG, "Requesting block list"); + this.sendIqPacket(getIqGenerator().generateGetBlockList(), mXmppConnectionService.getIqParser()); + } } private void sendServiceDiscoveryItems(final Jid server) { @@ -844,9 +785,9 @@ public class XmppConnection implements Runnable { this.sendIqPacket(iq, new OnIqPacketReceived() { @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - List<Element> elements = packet.query().getChildren(); - for (Element element : elements) { + public void onIqPacketReceived(final Account account, final IqPacket packet) { + final List<Element> elements = packet.query().getChildren(); + for (final Element element : elements) { if (element.getName().equals("item")) { final Jid jid = element.getAttributeAsJid("jid"); if (jid != null && !jid.equals(account.getServer())) { @@ -859,12 +800,12 @@ public class XmppConnection implements Runnable { } private void sendEnableCarbons() { - IqPacket iq = new IqPacket(IqPacket.TYPE_SET); + final IqPacket iq = new IqPacket(IqPacket.TYPE_SET); iq.addChild("enable", "urn:xmpp:carbons:2"); this.sendIqPacket(iq, new OnIqPacketReceived() { @Override - public void onIqPacketReceived(Account account, IqPacket packet) { + public void onIqPacketReceived(final Account account, final IqPacket packet) { if (!packet.hasChild("error")) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": successfully enabled carbons"); @@ -877,20 +818,20 @@ public class XmppConnection implements Runnable { }); } - private void processStreamError(Tag currentTag) + private void processStreamError(final Tag currentTag) throws XmlPullParserException, IOException { - Element streamError = tagReader.readElement(currentTag); + final Element streamError = tagReader.readElement(currentTag); if (streamError != null && streamError.hasChild("conflict")) { final String resource = account.getResource().split("\\.")[0]; account.setResource(resource + "." + nextRandomId()); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": switching resource due to conflict (" - + account.getResource() + ")"); + + account.getResource() + ")"); } } private void sendStartStream() throws IOException { - Tag stream = Tag.start("stream:stream"); + final Tag stream = Tag.start("stream:stream"); stream.setAttribute("from", account.getJid().toBareJid().toString()); stream.setAttribute("to", account.getServer().toString()); stream.setAttribute("version", "1.0"); @@ -904,33 +845,32 @@ public class XmppConnection implements Runnable { return new BigInteger(50, mXmppConnectionService.getRNG()).toString(32); } - public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) { + public void sendIqPacket(final IqPacket packet, final PacketReceived callback) { if (packet.getId() == null) { - String id = nextRandomId(); + final String id = nextRandomId(); packet.setAttribute("id", id); } packet.setFrom(account.getJid()); this.sendPacket(packet, callback); } - public void sendUnboundIqPacket(IqPacket packet, OnIqPacketReceived callback) { + public void sendUnboundIqPacket(final IqPacket packet, final PacketReceived callback) { if (packet.getId() == null) { - String id = nextRandomId(); + final String id = nextRandomId(); packet.setAttribute("id", id); } this.sendPacket(packet, callback); } - public void sendMessagePacket(MessagePacket packet) { + public void sendMessagePacket(final MessagePacket packet) { this.sendPacket(packet, null); } - public void sendPresencePacket(PresencePacket packet) { + public void sendPresencePacket(final PresencePacket packet) { this.sendPacket(packet, null); } - private synchronized void sendPacket(final AbstractStanza packet, - PacketReceived callback) { + private synchronized void sendPacket(final AbstractStanza packet, final PacketReceived callback) { if (packet.getName().equals("iq") || packet.getName().equals("message") || packet.getName().equals("presence")) { ++stanzasSent; @@ -955,7 +895,7 @@ public class XmppConnection implements Runnable { if (streamFeatures.hasChild("sm")) { tagWriter.writeStanzaAsync(new RequestPacket(smVersion)); } else { - IqPacket iq = new IqPacket(IqPacket.TYPE_GET); + final IqPacket iq = new IqPacket(IqPacket.TYPE_GET); iq.setFrom(account.getJid()); iq.addChild("ping", "urn:xmpp:ping"); this.sendIqPacket(iq, null); @@ -964,38 +904,44 @@ public class XmppConnection implements Runnable { } public void setOnMessagePacketReceivedListener( - OnMessagePacketReceived listener) { + final OnMessagePacketReceived listener) { this.messageListener = listener; } public void setOnUnregisteredIqPacketReceivedListener( - OnIqPacketReceived listener) { + final OnIqPacketReceived listener) { this.unregisteredIqListener = listener; } public void setOnPresencePacketReceivedListener( - OnPresencePacketReceived listener) { + final OnPresencePacketReceived listener) { this.presenceListener = listener; } public void setOnJinglePacketReceivedListener( - OnJinglePacketReceived listener) { + final OnJinglePacketReceived listener) { this.jingleListener = listener; } - public void setOnStatusChangedListener(OnStatusChanged listener) { + public void setOnStatusChangedListener(final OnStatusChanged listener) { this.statusListener = listener; } - public void setOnBindListener(OnBindListener listener) { + public void setOnBindListener(final OnBindListener listener) { this.bindListener = listener; } - public void setOnMessageAcknowledgeListener(OnMessageAcknowledged listener) { + public void setOnMessageAcknowledgeListener(final OnMessageAcknowledged listener) { this.acknowledgedListener = listener; } - public void disconnect(boolean force) { + public void addOnAdvancedStreamFeaturesAvailableListener(final OnAdvancedStreamFeaturesLoaded listener) { + if (!this.advancedStreamFeaturesLoadedListeners.contains(listener)) { + this.advancedStreamFeaturesLoadedListeners.add(listener); + } + } + + public void disconnect(final boolean force) { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": disconnecting"); try { if (force) { @@ -1015,23 +961,23 @@ public class XmppConnection implements Runnable { } tagWriter.writeTag(Tag.end("stream:stream")); socket.close(); - } catch (IOException e) { + } catch (final IOException e) { Log.d(Config.LOGTAG, "io exception during disconnect"); - } catch (InterruptedException e) { + } catch (final InterruptedException e) { Log.d(Config.LOGTAG, "interrupted"); } } } }).start(); - } catch (IOException e) { + } catch (final IOException e) { Log.d(Config.LOGTAG, "io exception during disconnect"); } } - public List<String> findDiscoItemsByFeature(String feature) { + public List<String> findDiscoItemsByFeature(final String feature) { final List<String> items = new ArrayList<>(); - for (Entry<String, List<String>> cursor : disco.entrySet()) { + for (final Entry<String, List<String>> cursor : disco.entrySet()) { if (cursor.getValue().contains(feature)) { items.add(cursor.getKey()); } @@ -1039,8 +985,8 @@ public class XmppConnection implements Runnable { return items; } - public String findDiscoItemByFeature(String feature) { - List<String> items = findDiscoItemsByFeature(feature); + public String findDiscoItemByFeature(final String feature) { + final List<String> items = findDiscoItemsByFeature(feature); if (items.size() >= 1) { return items.get(0); } @@ -1052,8 +998,7 @@ public class XmppConnection implements Runnable { } public String getMucServer() { - final List<String> items = new ArrayList<>(); - for (Entry<String, List<String>> cursor : disco.entrySet()) { + for (final Entry<String, List<String>> cursor : disco.entrySet()) { final List<String> value = cursor.getValue(); if (value.contains("http://jabber.org/protocol/muc") && !value.contains("jabber:iq:gateway") && !value.contains("siacs:no:muc")) { return cursor.getKey(); @@ -1063,8 +1008,8 @@ public class XmppConnection implements Runnable { } public int getTimeToNextAttempt() { - int interval = (int) (25 * Math.pow(1.5, attempt)); - int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); + final int interval = (int) (25 * Math.pow(1.5, attempt)); + final int secondsSinceLast = (int) ((SystemClock.elapsedRealtime() - this.lastConnect) / 1000); return interval - secondsSinceLast; } @@ -1077,7 +1022,7 @@ public class XmppConnection implements Runnable { } public long getLastSessionEstablished() { - long diff; + final long diff; if (this.lastSessionStarted == 0) { diff = SystemClock.elapsedRealtime() - this.lastConnect; } else { @@ -1109,7 +1054,7 @@ public class XmppConnection implements Runnable { public class Features { XmppConnection connection; - public Features(XmppConnection connection) { + public Features(final XmppConnection connection) { this.connection = connection; } @@ -1122,6 +1067,14 @@ public class XmppConnection implements Runnable { return hasDiscoFeature(account.getServer(), "urn:xmpp:carbons:2"); } + public boolean blocking() { + return hasDiscoFeature(account.getServer(), Xmlns.BLOCKING); + } + + public boolean register() { + return hasDiscoFeature(account.getServer(), Xmlns.REGISTER); + } + public boolean sm() { return streamId != null; } @@ -1139,6 +1092,10 @@ public class XmppConnection implements Runnable { return hasDiscoFeature(account.getServer(), "urn:xmpp:mam:0"); } + public boolean advancedStreamFeaturesLoaded() { + return disco.containsKey(account.getServer().toString()); + } + public boolean rosterVersioning() { return connection.streamFeatures != null && connection.streamFeatures.hasChild("ver"); } @@ -1147,9 +1104,9 @@ public class XmppConnection implements Runnable { return connection .findDiscoItemByFeature("http://jabber.org/protocol/bytestreams") != null; } + } - public boolean compression() { - return connection.enabledCompression; - } + private IqGenerator getIqGenerator() { + return mXmppConnectionService.getIqGenerator(); } } 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 ff9acb3f..44794c80 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -37,6 +37,7 @@ public class Data extends Element { Field field = getFieldByName(name); if (field == null) { field = new Field(name); + this.addChild(field); } field.setValue(value); } @@ -45,6 +46,7 @@ public class Data extends Element { Field field = getFieldByName(name); if (field == null) { field = new Field(name); + this.addChild(field); } field.setValues(values); } @@ -72,4 +74,12 @@ public class Data extends Element { data.setChildren(element.getChildren()); return data; } + + public void setFormType(String formType) { + this.put("FORM_TYPE",formType); + } + + public String getFormType() { + return this.getAttribute("FORM_TYPE"); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java index c40fa0b6..a35ea37c 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jid/Jid.java @@ -32,7 +32,7 @@ public final class Jid { return resourcepart; } - public static Jid fromSessionID(SessionID id) throws InvalidJidException{ + public static Jid fromSessionID(final SessionID id) throws InvalidJidException{ if (id.getUserID().isEmpty()) { return Jid.fromString(id.getAccountID()); } else { @@ -190,4 +190,8 @@ public final class Jid { public boolean isBareJid() { return this.resourcepart.isEmpty(); } + + public boolean isDomainJid() { + return !this.hasLocalpart(); + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 3a1ba778..d578ca38 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -191,10 +191,10 @@ public class JingleConnection implements Downloadable { } IqPacket response; if (returnResult) { - response = packet.generateRespone(IqPacket.TYPE_RESULT); + response = packet.generateResponse(IqPacket.TYPE_RESULT); } else { - response = packet.generateRespone(IqPacket.TYPE_ERROR); + response = packet.generateResponse(IqPacket.TYPE_ERROR); } account.getXmppConnection().sendIqPacket(response, null); } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index 72c960d8..b0a730b1 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -46,7 +46,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return; } } - IqPacket response = packet.generateRespone(IqPacket.TYPE_ERROR); + IqPacket response = packet.generateResponse(IqPacket.TYPE_ERROR); Element error = response.addChild("error"); error.setAttribute("type", "cancel"); error.addChild("item-not-found", 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 04b225d0..e25f7e65 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleInbandTransport.java @@ -208,15 +208,15 @@ public class JingleInbandTransport extends JingleTransport { established = true; connected = true; this.account.getXmppConnection().sendIqPacket( - packet.generateRespone(IqPacket.TYPE_RESULT), null); + packet.generateResponse(IqPacket.TYPE_RESULT), null); } else { this.account.getXmppConnection().sendIqPacket( - packet.generateRespone(IqPacket.TYPE_ERROR), null); + packet.generateResponse(IqPacket.TYPE_ERROR), null); } } else if (connected && payload.getName().equals("data")) { this.receiveNextBlock(payload.getContent()); this.account.getXmppConnection().sendIqPacket( - packet.generateRespone(IqPacket.TYPE_RESULT), null); + packet.generateResponse(IqPacket.TYPE_RESULT), null); } else { // TODO some sort of exception } 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 9e051472..1a49b45e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/AbstractStanza.java @@ -1,7 +1,6 @@ package eu.siacs.conversations.xmpp.stanzas; import eu.siacs.conversations.xml.Element; -import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; public class AbstractStanza extends Element { @@ -11,24 +10,11 @@ public class AbstractStanza extends Element { } public Jid getTo() { - try { - return Jid.fromString(getAttribute("to")); - } catch (final InvalidJidException e) { - return null; - } + return getAttributeAsJid("to"); } public Jid getFrom() { - String from = getAttribute("from"); - if (from == null) { - return null; - } else { - try { - return Jid.fromString(from); - } catch (final InvalidJidException e) { - return null; - } - } + return getAttributeAsJid("from"); } public String getId() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java index 9df05e67..2481112b 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java +++ b/src/main/java/eu/siacs/conversations/xmpp/stanzas/IqPacket.java @@ -9,27 +9,27 @@ public class IqPacket extends AbstractStanza { public static final int TYPE_RESULT = 1; public static final int TYPE_GET = 2; - private IqPacket(String name) { + private IqPacket(final String name) { super(name); } - public IqPacket(int type) { + public IqPacket(final int type) { super("iq"); switch (type) { - case TYPE_SET: - this.setAttribute("type", "set"); - break; - case TYPE_GET: - this.setAttribute("type", "get"); - break; - case TYPE_RESULT: - this.setAttribute("type", "result"); - break; - case TYPE_ERROR: - this.setAttribute("type", "error"); - break; - default: - break; + case TYPE_SET: + this.setAttribute("type", "set"); + break; + case TYPE_GET: + this.setAttribute("type", "get"); + break; + case TYPE_RESULT: + this.setAttribute("type", "result"); + break; + case TYPE_ERROR: + this.setAttribute("type", "error"); + break; + default: + break; } } @@ -45,29 +45,30 @@ public class IqPacket extends AbstractStanza { return query; } - public Element query(String xmlns) { - Element query = query(); + public Element query(final String xmlns) { + final Element query = query(); query.setAttribute("xmlns", xmlns); return query(); } public int getType() { - String type = getAttribute("type"); - if ("error".equals(type)) { - return TYPE_ERROR; - } else if ("result".equals(type)) { - return TYPE_RESULT; - } else if ("set".equals(type)) { - return TYPE_SET; - } else if ("get".equals(type)) { - return TYPE_GET; - } else { - return 1000; + final String type = getAttribute("type"); + switch (type) { + case "error": + return TYPE_ERROR; + case "result": + return TYPE_RESULT; + case "set": + return TYPE_SET; + case "get": + return TYPE_GET; + default: + return 1000; } } - public IqPacket generateRespone(int type) { - IqPacket packet = new IqPacket(type); + public IqPacket generateResponse(final int type) { + final IqPacket packet = new IqPacket(type); packet.setTo(this.getFrom()); packet.setId(this.getId()); return packet; |