diff options
author | Christian S <christian@pix-art.de> | 2016-02-04 18:02:03 +0100 |
---|---|---|
committer | Christian S <christian@pix-art.de> | 2016-02-04 18:02:03 +0100 |
commit | 14b688e0fbe62a0fef0fadcc20763faa21801d7a (patch) | |
tree | 37fc7b87aa21326b0ca7ba97fee84aaf0ea6d3c2 /src/main/java | |
parent | 9e96d80e31c5d0455c4b15984eef5709cf1071fc (diff) | |
parent | 91ec4839acef3e395e83815f211dd9771af1287e (diff) |
Merge remote-tracking branch 'siacs/master' into development
Diffstat (limited to 'src/main/java')
27 files changed, 868 insertions, 326 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 7d22057b0..125facf07 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -68,6 +68,8 @@ public final class Config { public static final boolean IGNORE_ID_REWRITE_IN_MUC = true; + public static final boolean REQUEST_DISCO = true; + 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; diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 43a900109..949975aa4 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.crypto.axolotl; +import android.os.Bundle; import android.security.KeyChain; import android.support.annotation.NonNull; import android.support.annotation.Nullable; @@ -39,6 +40,7 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.parser.IqParser; import eu.siacs.conversations.services.XmppConnectionService; +import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.SerialSingleThreadExecutor; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.OnAdvancedStreamFeaturesLoaded; @@ -160,6 +162,20 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { AxolotlAddress axolotlAddress = new AxolotlAddress(bareJid, deviceId); Log.d(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Building session for remote address: " + axolotlAddress.toString()); IdentityKey identityKey = store.loadSession(axolotlAddress).getSessionState().getRemoteIdentityKey(); + if(Config.X509_VERIFICATION) { + X509Certificate certificate = store.getFingerprintCertificate(identityKey.getFingerprint().replaceAll("\\s", "")); + if (certificate != null) { + Bundle information = CryptoHelper.extractCertificateInformation(certificate); + try { + final String cn = information.getString("subject_cn"); + final Jid jid = Jid.fromString(bareJid); + Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); + account.getRoster().getContact(jid).setCommonName(cn); + } catch (final InvalidJidException ignored) { + //ignored + } + } + } this.put(axolotlAddress, new XmppAxolotlSession(account, store, axolotlAddress, identityKey)); } } @@ -619,6 +635,15 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { setFingerprintTrust(fingerprint, XmppAxolotlSession.Trust.TRUSTED_X509); axolotlStore.setFingerprintCertificate(fingerprint, verification.first[0]); fetchStatusMap.put(address, FetchStatus.SUCCESS_VERIFIED); + Bundle information = CryptoHelper.extractCertificateInformation(verification.first[0]); + try { + final String cn = information.getString("subject_cn"); + final Jid jid = Jid.fromString(address.getName()); + Log.d(Config.LOGTAG,"setting common name for "+jid+" to "+cn); + account.getRoster().getContact(jid).setCommonName(cn); + } catch (final InvalidJidException ignored) { + //ignored + } finishBuildingSessionsFromPEP(address); return; } catch (Exception e) { diff --git a/src/main/java/eu/siacs/conversations/entities/Account.java b/src/main/java/eu/siacs/conversations/entities/Account.java index 4abfc801a..356b34e57 100644 --- a/src/main/java/eu/siacs/conversations/entities/Account.java +++ b/src/main/java/eu/siacs/conversations/entities/Account.java @@ -3,6 +3,7 @@ package eu.siacs.conversations.entities; import android.content.ContentValues; import android.database.Cursor; import android.os.SystemClock; +import android.util.Pair; import eu.siacs.conversations.crypto.PgpDecryptionService; import net.java.otr4j.crypto.OtrCryptoEngineImpl; @@ -14,6 +15,7 @@ import org.json.JSONObject; import java.security.PublicKey; import java.security.interfaces.DSAPublicKey; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; @@ -48,6 +50,7 @@ public class Account extends AbstractEntity { public static final int OPTION_DISABLED = 1; public static final int OPTION_REGISTER = 2; public static final int OPTION_USECOMPRESSION = 3; + public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>(); public boolean httpUploadAvailable() { return xmppConnection != null && xmppConnection.getFeatures().httpUpload(); diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 9c8beb1ec..91ceebffb 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -38,6 +38,7 @@ public class Contact implements ListItem, Blockable { protected String systemName; protected String serverName; protected String presenceName; + protected String commonName; protected Jid jid; protected int subscription = 0; protected String systemAccount; @@ -105,8 +106,8 @@ public class Contact implements ListItem, Blockable { } public String getDisplayName() { - if (this.presenceName != null && Config.X509_VERIFICATION) { - return this.presenceName; + if (this.commonName != null && Config.X509_VERIFICATION) { + return this.commonName; } else if (this.systemName != null) { return this.systemName; } else if (this.serverName != null) { @@ -135,17 +136,17 @@ public class Contact implements ListItem, Blockable { tags.add(new Tag(group, UIHelper.getColorForName(group))); } switch (getMostAvailableStatus()) { - case Presences.CHAT: - case Presences.ONLINE: + case CHAT: + case ONLINE: tags.add(new Tag("online", 0xff259b24)); break; - case Presences.AWAY: + case AWAY: tags.add(new Tag("away", 0xffff9800)); break; - case Presences.XA: + case XA: tags.add(new Tag("not available", 0xfff44336)); break; - case Presences.DND: + case DND: tags.add(new Tag("dnd", 0xfff44336)); break; } @@ -228,8 +229,8 @@ public class Contact implements ListItem, Blockable { this.presences = pres; } - public void updatePresence(final String resource, final int status) { - this.presences.updatePresence(resource, status); + public void updatePresence(final String resource, final Presence presence) { + this.presences.updatePresence(resource, presence); } public void removePresence(final String resource) { @@ -241,8 +242,13 @@ public class Contact implements ListItem, Blockable { this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); } - public int getMostAvailableStatus() { - return this.presences.getMostAvailableStatus(); + public Presence.Status getMostAvailableStatus() { + Presence p = this.presences.getMostAvailablePresence(); + if (p == null) { + return Presence.Status.OFFLINE; + } + + return p.getStatus(); } public String getMostAvailableResource() { @@ -517,6 +523,10 @@ public class Contact implements ListItem, Blockable { return account.getJid().toBareJid().equals(getJid().toBareJid()); } + public void setCommonName(String cn) { + this.commonName = cn; + } + public static class Lastseen { public long time; public String presence; diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index d2f3e69ea..1e18f6967 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -82,6 +82,7 @@ public class Conversation extends AbstractEntity implements Blockable { private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE; private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE; private String mLastReceivedOtrMessageId = null; + private String mFirstMamReference = null; public boolean hasMessagesLeftOnServer() { return messagesLeftOnServer; @@ -278,6 +279,22 @@ public class Conversation extends AbstractEntity implements Blockable { } } + public void setFirstMamReference(String reference) { + this.mFirstMamReference = reference; + } + + public String getFirstMamReference() { + return this.mFirstMamReference; + } + + public void setLastClearHistory(long time) { + setAttribute("last_clear_history",String.valueOf(time)); + } + + public long getLastClearHistory() { + return getLongAttribute("last_clear_history", 0); + } + public interface OnMessageFound { void onMessageFound(final Message message); } @@ -725,6 +742,10 @@ public class Conversation extends AbstractEntity implements Blockable { } public long getLastMessageTransmitted() { + long last_clear = getLastClearHistory(); + if (last_clear != 0) { + return last_clear; + } synchronized (this.messages) { for(int i = this.messages.size() - 1; i >= 0; --i) { Message message = this.messages.get(i); @@ -745,7 +766,7 @@ public class Conversation extends AbstractEntity implements Blockable { } public boolean alwaysNotify() { - return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY,Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA()); + return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA()); } public boolean setAttribute(String key, String value) { @@ -807,6 +828,13 @@ public class Conversation extends AbstractEntity implements Blockable { } } + public void prepend(Message message) { + message.setConversation(this); + synchronized (this.messages) { + this.messages.add(0,message); + } + } + public void addAll(int index, List<Message> messages) { synchronized (this.messages) { this.messages.addAll(index, messages); diff --git a/src/main/java/eu/siacs/conversations/entities/Message.java b/src/main/java/eu/siacs/conversations/entities/Message.java index f3d891e86..f37ae4271 100644 --- a/src/main/java/eu/siacs/conversations/entities/Message.java +++ b/src/main/java/eu/siacs/conversations/entities/Message.java @@ -178,6 +178,14 @@ public class Message extends AbstractEntity { return message; } + public static Message createLoadMoreMessage(Conversation conversation) { + final Message message = new Message(); + message.setType(Message.TYPE_STATUS); + message.setConversation(conversation); + message.setBody("LOAD_MORE"); + return message; + } + @Override public ContentValues getContentValues() { ContentValues values = new ContentValues(); diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 014a4c985..7f4ded119 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -3,7 +3,10 @@ package eu.siacs.conversations.entities; import android.annotation.SuppressLint; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import eu.siacs.conversations.R; import eu.siacs.conversations.xmpp.forms.Data; @@ -64,7 +67,7 @@ public class MucOptions { PARTICIPANT("participant", R.string.participant,2), NONE("none", R.string.no_role,0); - private Role(String string, int resId, int rank) { + Role(String string, int resId, int rank) { this.string = string; this.resId = resId; this.rank = rank; @@ -94,6 +97,7 @@ public class MucOptions { public static final int ERROR_PASSWORD_REQUIRED = 3; public static final int ERROR_BANNED = 4; public static final int ERROR_MEMBERS_ONLY = 5; + public static final int ERROR_NO_RESPONSE = 6; public static final int KICKED_FROM_ROOM = 9; @@ -236,12 +240,12 @@ public class MucOptions { } private Account account; - private final List<User> users = new ArrayList<>(); + private final Map<String, User> users = Collections.synchronizedMap(new LinkedHashMap<String, User>()); private List<String> features = new ArrayList<>(); private Data form = new Data(); private Conversation conversation; private boolean isOnline = false; - private int error = ERROR_UNKNOWN; + private int error = ERROR_NO_RESPONSE; public OnRenameListener onRenameListener = null; private User self; private String subject = null; @@ -303,40 +307,15 @@ public class MucOptions { } public User deleteUser(String name) { - synchronized (this.users) { - for (int i = 0; i < users.size(); ++i) { - if (users.get(i).getName().equals(name)) { - return users.remove(i); - } - } - } - return null; + return this.users.remove(name); } public void addUser(User user) { - synchronized (this.users) { - for (int i = 0; i < users.size(); ++i) { - if (users.get(i).getName().equals(user.getName())) { - users.set(i, user); - return; - } - } - users.add(user); - } + this.users.put(user.getName(), user); } public User findUser(String name) { - if (name == null) { - return null; - } - synchronized (this.users) { - for (User user : users) { - if (user.getName().equals(name)) { - return user; - } - } - } - return null; + return this.users.get(name); } public boolean isUserInRoom(String name) { @@ -344,26 +323,34 @@ public class MucOptions { } public void setError(int error) { - this.isOnline = error == ERROR_NO_ERROR; + this.isOnline = isOnline && error == ERROR_NO_ERROR; this.error = error; } + public void setOnline() { + this.isOnline = true; + } + public ArrayList<User> getUsers() { - synchronized (this.users) { - return new ArrayList(this.users); - } + return new ArrayList<>(users.values()); } public List<User> getUsers(int max) { - synchronized (this.users) { - return new ArrayList<>(users.subList(0,Math.min(users.size(),5))); + ArrayList<User> users = new ArrayList<>(); + int i = 1; + for(User user : this.users.values()) { + users.add(user); + if (i >= max) { + break; + } else { + ++i; + } } + return users; } public int getUserCount() { - synchronized (this.users) { - return this.users.size(); - } + return this.users.size(); } public String getProposedNick() { @@ -400,7 +387,7 @@ public class MucOptions { public void setOffline() { this.users.clear(); - this.error = 0; + this.error = ERROR_NO_RESPONSE; this.isOnline = false; } @@ -417,38 +404,34 @@ public class MucOptions { } public String createNameFromParticipants() { - synchronized (this.users) { - 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()); - } + if (users.size() >= 2) { + List<String> names = new ArrayList<>(); + for (User user : getUsers(5)) { + 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)); - if (i != names.size() - 1) { - builder.append(", "); - } + } + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < names.size(); ++i) { + builder.append(names.get(i)); + if (i != names.size() - 1) { + builder.append(", "); } - return builder.toString(); - } else { - return null; } + return builder.toString(); + } else { + return null; } } public long[] getPgpKeyIds() { List<Long> ids = new ArrayList<>(); - synchronized (this.users) { - for (User user : this.users) { - if (user.getPgpKeyId() != 0) { - ids.add(user.getPgpKeyId()); - } + for (User user : this.users.values()) { + if (user.getPgpKeyId() != 0) { + ids.add(user.getPgpKeyId()); } } ids.add(account.getPgpId()); @@ -460,22 +443,18 @@ public class MucOptions { } public boolean pgpKeysInUse() { - synchronized (this.users) { - for (User user : this.users) { - if (user.getPgpKeyId() != 0) { - return true; - } + for (User user : this.users.values()) { + if (user.getPgpKeyId() != 0) { + return true; } } return false; } public boolean everybodyHasKeys() { - synchronized (this.users) { - for (User user : this.users) { - if (user.getPgpKeyId() == 0) { - return false; - } + for (User user : this.users.values()) { + if (user.getPgpKeyId() == 0) { + return false; } } return true; @@ -489,15 +468,9 @@ public class MucOptions { } } - public Jid getTrueCounterpart(String counterpart) { - synchronized (this.users) { - for (User user : this.users) { - if (user.getName().equals(counterpart)) { - return user.getJid(); - } - } - } - return null; + public Jid getTrueCounterpart(String name) { + User user = findUser(name); + return user == null ? null : user.getJid(); } public String getPassword() { diff --git a/src/main/java/eu/siacs/conversations/entities/Presence.java b/src/main/java/eu/siacs/conversations/entities/Presence.java new file mode 100644 index 000000000..69cde8327 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Presence.java @@ -0,0 +1,76 @@ +package eu.siacs.conversations.entities; + +import java.lang.Comparable; + +import eu.siacs.conversations.xml.Element; + +public class Presence implements Comparable { + + public enum Status { + CHAT, ONLINE, AWAY, XA, DND, OFFLINE; + + public String toShowString() { + switch(this) { + case CHAT: return "chat"; + case AWAY: return "away"; + case XA: return "xa"; + case DND: return "dnd"; + } + + return null; + } + } + + protected final Status status; + protected ServiceDiscoveryResult disco; + protected final String ver; + protected final String hash; + + private Presence(Status status, String ver, String hash) { + this.status = status; + this.ver = ver; + this.hash = hash; + } + + public static Presence parse(Element show, Element caps) { + final String hash = caps == null ? null : caps.getAttribute("hash"); + final String ver = caps == null ? null : caps.getAttribute("ver"); + if ((show == null) || (show.getContent() == null)) { + return new Presence(Status.ONLINE, ver, hash); + } else if (show.getContent().equals("away")) { + return new Presence(Status.AWAY, ver, hash); + } else if (show.getContent().equals("xa")) { + return new Presence(Status.XA, ver, hash); + } else if (show.getContent().equals("chat")) { + return new Presence(Status.CHAT, ver, hash); + } else if (show.getContent().equals("dnd")) { + return new Presence(Status.DND, ver, hash); + } else { + return new Presence(Status.OFFLINE, ver, hash); + } + } + + public int compareTo(Object other) { + return this.status.compareTo(((Presence)other).status); + } + + public Status getStatus() { + return this.status; + } + + public boolean hasCaps() { + return ver != null && hash != null; + } + + public String getVer() { + return this.ver; + } + + public String getHash() { + return this.hash; + } + + public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) { + this.disco = disco; + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java index a1e90d170..813eda7a9 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presences.java +++ b/src/main/java/eu/siacs/conversations/entities/Presences.java @@ -1,29 +1,21 @@ package eu.siacs.conversations.entities; +import java.util.Collections; import java.util.Hashtable; import java.util.Iterator; -import java.util.Map.Entry; import eu.siacs.conversations.xml.Element; public class Presences { + private final Hashtable<String, Presence> presences = new Hashtable<>(); - public static final int CHAT = -1; - public static final int ONLINE = 0; - public static final int AWAY = 1; - public static final int XA = 2; - public static final int DND = 3; - public static final int OFFLINE = 4; - - private final Hashtable<String, Integer> presences = new Hashtable<>(); - - public Hashtable<String, Integer> getPresences() { + public Hashtable<String, Presence> getPresences() { return this.presences; } - public void updatePresence(String resource, int status) { + public void updatePresence(String resource, Presence presence) { synchronized (this.presences) { - this.presences.put(resource, status); + this.presences.put(resource, presence); } } @@ -39,47 +31,10 @@ public class Presences { } } - public int getMostAvailableStatus() { - int status = OFFLINE; + public Presence getMostAvailablePresence() { synchronized (this.presences) { - Iterator<Entry<String, Integer>> it = presences.entrySet().iterator(); - while (it.hasNext()) { - Entry<String, Integer> entry = it.next(); - if (entry.getValue() < status) - status = entry.getValue(); - } - } - return status; - } - - public String getMostAvailableResource() { - int status = OFFLINE; - String resource = ""; - synchronized (this.presences) { - Iterator<Entry<String, Integer>> it = presences.entrySet().iterator(); - while (it.hasNext()) { - Entry<String, Integer> entry = it.next(); - if (entry.getValue() < status) - status = entry.getValue(); - resource = entry.getKey(); - } - } - return resource; - } - - public static int parseShow(Element show) { - if ((show == null) || (show.getContent() == null)) { - return Presences.ONLINE; - } else if (show.getContent().equals("away")) { - return Presences.AWAY; - } else if (show.getContent().equals("xa")) { - return Presences.XA; - } else if (show.getContent().equals("chat")) { - return Presences.CHAT; - } else if (show.getContent().equals("dnd")) { - return Presences.DND; - } else { - return Presences.OFFLINE; + if (presences.size() < 1) { return null; } + return Collections.min(presences.values()); } } diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java new file mode 100644 index 000000000..42f2d8409 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -0,0 +1,265 @@ +package eu.siacs.conversations.entities; + +import android.content.ContentValues; +import android.database.Cursor; +import android.util.Base64; +import java.io.UnsupportedEncodingException; +import java.lang.Comparable; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.forms.Data; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class ServiceDiscoveryResult { + public static final String TABLENAME = "discovery_results"; + public static final String HASH = "hash"; + public static final String VER = "ver"; + public static final String RESULT = "result"; + + protected static String blankNull(String s) { + return s == null ? "" : s; + } + + public static class Identity implements Comparable { + protected final String category; + protected final String type; + protected final String lang; + protected final String name; + + public Identity(final String category, final String type, final String lang, final String name) { + this.category = category; + this.type = type; + this.lang = lang; + this.name = name; + } + + public Identity(final Element el) { + this( + el.getAttribute("category"), + el.getAttribute("type"), + el.getAttribute("xml:lang"), + el.getAttribute("name") + ); + } + + public Identity(final JSONObject o) { + this( + o.optString("category", null), + o.optString("type", null), + o.optString("lang", null), + o.optString("name", null) + ); + } + + public String getCategory() { + return this.category; + } + + public String getType() { + return this.type; + } + + public String getLang() { + return this.lang; + } + + public String getName() { + return this.name; + } + + public int compareTo(Object other) { + Identity o = (Identity)other; + int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory())); + if(r == 0) { + r = blankNull(this.getType()).compareTo(blankNull(o.getType())); + } + if(r == 0) { + r = blankNull(this.getLang()).compareTo(blankNull(o.getLang())); + } + if(r == 0) { + r = blankNull(this.getName()).compareTo(blankNull(o.getName())); + } + + return r; + } + + public JSONObject toJSON() { + try { + JSONObject o = new JSONObject(); + o.put("category", this.getCategory()); + o.put("type", this.getType()); + o.put("lang", this.getLang()); + o.put("name", this.getName()); + return o; + } catch(JSONException e) { + return null; + } + } + } + + protected final String hash; + protected final byte[] ver; + protected final List<Identity> identities; + protected final List<String> features; + protected final List<Data> forms; + + public ServiceDiscoveryResult(final IqPacket packet) { + this.identities = new ArrayList<>(); + this.features = new ArrayList<>(); + this.forms = new ArrayList<>(); + this.hash = "sha-1"; // We only support sha-1 for now + + final List<Element> elements = packet.query().getChildren(); + + for (final Element element : elements) { + if (element.getName().equals("identity")) { + Identity id = new Identity(element); + if (id.getType() != null && id.getCategory() != null) { + identities.add(id); + } + } else if (element.getName().equals("feature")) { + if (element.getAttribute("var") != null) { + features.add(element.getAttribute("var")); + } + } else if (element.getName().equals("x") && "jabber:x:data".equals(element.getAttribute("xmlns"))) { + forms.add(Data.parse(element)); + } + } + this.ver = this.mkCapHash(); + } + + public ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException { + this.identities = new ArrayList<>(); + this.features = new ArrayList<>(); + this.forms = new ArrayList<>(); + this.hash = hash; + this.ver = ver; + + JSONArray identities = o.optJSONArray("identities"); + if (identities != null) { + for (int i = 0; i < identities.length(); i++) { + this.identities.add(new Identity(identities.getJSONObject(i))); + } + } + JSONArray features = o.optJSONArray("features"); + if (features != null) { + for (int i = 0; i < features.length(); i++) { + this.features.add(features.getString(i)); + } + } + } + + public String getVer() { + return new String(Base64.encode(this.ver, Base64.DEFAULT)).trim(); + } + + public ServiceDiscoveryResult(Cursor cursor) throws JSONException { + this( + cursor.getString(cursor.getColumnIndex(HASH)), + Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT), + new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT))) + ); + } + + public List<Identity> getIdentities() { + return this.identities; + } + + public List<String> getFeatures() { + return this.features; + } + + public boolean hasIdentity(String category, String type) { + for(Identity id : this.getIdentities()) { + if((category == null || id.getCategory().equals(category)) && + (type == null || id.getType().equals(type))) { + return true; + } + } + + return false; + } + + protected byte[] mkCapHash() { + StringBuilder s = new StringBuilder(); + + List<Identity> identities = this.getIdentities(); + Collections.sort(identities); + + for(Identity id : identities) { + s.append( + blankNull(id.getCategory()) + "/" + + blankNull(id.getType()) + "/" + + blankNull(id.getLang()) + "/" + + blankNull(id.getName()) + "<" + ); + } + + List<String> features = this.getFeatures(); + Collections.sort(features); + + for (String feature : features) { + s.append(feature + "<"); + } + + Collections.sort(forms, new Comparator<Data>() { + @Override + public int compare(Data lhs, Data rhs) { + return lhs.getFormType().compareTo(rhs.getFormType()); + } + }); + + for(Data form : forms) { + s.append(form.getFormType()+"<"); + //TODO append fields and values + } + + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + + try { + return md.digest(s.toString().getBytes("UTF-8")); + } catch(UnsupportedEncodingException e) { + return null; + } + } + + public JSONObject toJSON() { + try { + JSONObject o = new JSONObject(); + + JSONArray ids = new JSONArray(); + for(Identity id : this.getIdentities()) { + ids.put(id.toJSON()); + } + o.put("identites", ids); + + o.put("features", new JSONArray(this.getFeatures())); + + return o; + } catch(JSONException e) { + return null; + } + } + + public ContentValues getContentValues() { + final ContentValues values = new ContentValues(); + values.put(HASH, this.hash); + values.put(VER, getVer()); + values.put(RESULT, this.toJSON().toString()); + return values; + } +} diff --git a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java index fdfde88c7..093a8963f 100644 --- a/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java +++ b/src/main/java/eu/siacs/conversations/generator/PresenceGenerator.java @@ -2,7 +2,7 @@ package eu.siacs.conversations.generator; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; -import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; @@ -37,21 +37,10 @@ public class PresenceGenerator extends AbstractGenerator { return subscription("subscribed", contact); } - public PresencePacket selfPresence(Account account, int presence) { + public PresencePacket selfPresence(Account account, Presence.Status status) { PresencePacket packet = new PresencePacket(); - switch(presence) { - case Presences.AWAY: - packet.addChild("show").setContent("away"); - break; - case Presences.XA: - packet.addChild("show").setContent("xa"); - break; - case Presences.CHAT: - packet.addChild("show").setContent("chat"); - break; - case Presences.DND: - packet.addChild("show").setContent("dnd"); - break; + if(status.toShowString() != null) { + packet.addChild("show").setContent(status.toShowString()); } packet.setFrom(account.getJid()); String sig = account.getPgpSignature(); diff --git a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java index 910c43f31..a8b31a7a9 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/http/HttpConnectionManager.java @@ -23,6 +23,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.services.AbstractConnectionManager; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; +import eu.siacs.conversations.utils.SSLSocketHelper; public class HttpConnectionManager extends AbstractConnectionManager { @@ -76,7 +77,7 @@ public class HttpConnectionManager extends AbstractConnectionManager { new StrictHostnameVerifier()); } try { - final SSLContext sc = SSLContext.getInstance("TLS"); + final SSLContext sc = SSLSocketHelper.getSSLContext(); sc.init(null, new X509TrustManager[]{trustManager}, mXmppConnectionService.getRNG()); diff --git a/src/main/java/eu/siacs/conversations/parser/IqParser.java b/src/main/java/eu/siacs/conversations/parser/IqParser.java index 1761e0df1..09bbabebe 100644 --- a/src/main/java/eu/siacs/conversations/parser/IqParser.java +++ b/src/main/java/eu/siacs/conversations/parser/IqParser.java @@ -142,7 +142,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } try { publicKey = Curve.decodePoint(Base64.decode(signedPreKeyPublic.getContent(),Base64.DEFAULT), 0); - } catch (InvalidKeyException | IllegalArgumentException e) { + } catch (InvalidKeyException | IllegalArgumentException | NullPointerException e) { Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid signedPreKeyPublic in PEP: " + e.getMessage()); } return publicKey; @@ -169,7 +169,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { } try { identityKey = new IdentityKey(Base64.decode(identityKeyElement.getContent(), Base64.DEFAULT), 0); - } catch (InvalidKeyException | IllegalArgumentException e) { + } catch (InvalidKeyException | IllegalArgumentException | NullPointerException e) { Log.e(Config.LOGTAG,AxolotlService.LOGPREFIX+" : "+"Invalid identityKey in PEP: "+e.getMessage()); } return identityKey; @@ -200,7 +200,7 @@ public class IqParser extends AbstractParser implements OnIqPacketReceived { try { ECPublicKey preKeyPublic = Curve.decodePoint(Base64.decode(preKeyPublicElement.getContent(), Base64.DEFAULT), 0); preKeyRecords.put(preKeyId, preKeyPublic); - } catch (InvalidKeyException | IllegalArgumentException e) { + } catch (InvalidKeyException | IllegalArgumentException | NullPointerException e) { Log.e(Config.LOGTAG, AxolotlService.LOGPREFIX+" : "+"Invalid preKeyPublic (ID="+preKeyId+") in PEP: "+ e.getMessage()+", skipping..."); continue; } diff --git a/src/main/java/eu/siacs/conversations/parser/MessageParser.java b/src/main/java/eu/siacs/conversations/parser/MessageParser.java index 345e83958..3bf488316 100644 --- a/src/main/java/eu/siacs/conversations/parser/MessageParser.java +++ b/src/main/java/eu/siacs/conversations/parser/MessageParser.java @@ -271,7 +271,7 @@ public class MessageParser extends AbstractParser implements packet = f.first; isForwarded = true; serverMsgId = result.getAttribute("id"); - query.incrementTotalCount(); + query.incrementMessageCount(); } else if (query != null) { Log.d(Config.LOGTAG,account.getJid().toBareJid()+": received mam result from invalid sender"); return; @@ -398,7 +398,11 @@ public class MessageParser extends AbstractParser implements return; } - conversation.add(message); + if (query != null && query.getPagingOrder() == MessageArchiveService.PagingOrder.REVERSE) { + conversation.prepend(message); + } else { + conversation.add(message); + } if (query == null || query.getWith() == null) { //either no mam or catchup if (status == Message.STATUS_SEND || status == Message.STATUS_SEND_RECEIVED) { @@ -411,9 +415,7 @@ public class MessageParser extends AbstractParser implements } } - if (query != null) { - query.incrementMessageCount(); - } else { + if (query == null) { mXmppConnectionService.updateConversationUi(); } @@ -454,7 +456,7 @@ public class MessageParser extends AbstractParser implements mXmppConnectionService.getNotificationService().pushFromBacklog(message); } } - } else { //no body + } else if (!packet.hasChild("body")){ //no body if (isTypeGroupChat) { Conversation conversation = mXmppConnectionService.find(account, from.toBareJid()); if (packet.hasChild("subject")) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index d27182c19..45c31e2b0 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -1,21 +1,28 @@ package eu.siacs.conversations.parser; +import android.util.Log; + import java.util.ArrayList; import java.util.List; + +import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; -import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.entities.Presence; +import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.generator.PresenceGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.OnIqPacketReceived; import eu.siacs.conversations.xmpp.OnPresencePacketReceived; import eu.siacs.conversations.xmpp.jid.Jid; import eu.siacs.conversations.xmpp.pep.Avatar; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; import eu.siacs.conversations.xmpp.stanzas.PresencePacket; public class PresenceParser extends AbstractParser implements @@ -56,12 +63,13 @@ public class PresenceParser extends AbstractParser implements if (x != null) { Element item = x.findChild("item"); if (item != null && !from.isBareJid()) { + mucOptions.setError(MucOptions.ERROR_NO_ERROR); MucOptions.User user = new MucOptions.User(mucOptions,from); user.setAffiliation(item.getAttribute("affiliation")); user.setRole(item.getAttribute("role")); user.setJid(item.getAttributeAsJid("jid")); if (codes.contains(MucOptions.STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(mucOptions.getConversation().getJid())) { - mucOptions.setError(MucOptions.ERROR_NO_ERROR); + mucOptions.setOnline(); mucOptions.setSelf(user); if (mucOptions.mNickChangingInProgress) { if (mucOptions.onRenameListener != null) { @@ -108,6 +116,7 @@ public class PresenceParser extends AbstractParser implements mucOptions.setError(MucOptions.ERROR_MEMBERS_ONLY); } else { mucOptions.setError(MucOptions.ERROR_UNKNOWN); + Log.d(Config.LOGTAG, "unknown error in conference: " + packet); } } else if (!from.isBareJid()){ MucOptions.User user = mucOptions.deleteUser(from.getResourcepart()); @@ -160,7 +169,7 @@ public class PresenceParser extends AbstractParser implements final String type = packet.getAttribute("type"); final Contact contact = account.getRoster().getContact(from); if (type == null) { - String presence = from.isBareJid() ? "" : from.getResourcepart(); + final String resource = from.isBareJid() ? "" : from.getResourcepart(); contact.setPresenceName(packet.findChildContent("nick", "http://jabber.org/protocol/nick")); Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); if (avatar != null && !contact.isSelf()) { @@ -176,7 +185,15 @@ public class PresenceParser extends AbstractParser implements } } int sizeBefore = contact.getPresences().size(); - contact.updatePresence(presence, Presences.parseShow(packet.findChild("show"))); + + final Element show = packet.findChild("show"); + final Element caps = packet.findChild("c", "http://jabber.org/protocol/caps"); + final Presence presence = Presence.parse(show, caps); + contact.updatePresence(resource, presence); + if (presence.hasCaps() && Config.REQUEST_DISCO) { + mXmppConnectionService.fetchCaps(account, from, presence); + } + PgpEngine pgp = mXmppConnectionService.getPgpEngine(); Element x = packet.findChild("x", "jabber:x:signed"); if (pgp != null && x != null) { diff --git a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java index 77c16f25b..2f28a30ff 100644 --- a/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java @@ -31,6 +31,7 @@ import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; +import org.json.JSONException; import eu.siacs.conversations.Config; import eu.siacs.conversations.crypto.axolotl.AxolotlService; @@ -41,6 +42,7 @@ import eu.siacs.conversations.entities.Contact; import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Roster; +import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.xmpp.jid.InvalidJidException; import eu.siacs.conversations.xmpp.jid.Jid; @@ -49,7 +51,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { private static DatabaseBackend instance = null; private static final String DATABASE_NAME = "history"; - private static final int DATABASE_VERSION = 22; + private static final int DATABASE_VERSION = 23; private static String CREATE_CONTATCS_STATEMENT = "create table " + Contact.TABLENAME + "(" + Contact.ACCOUNT + " TEXT, " @@ -63,6 +65,14 @@ public class DatabaseBackend extends SQLiteOpenHelper { + ") ON DELETE CASCADE, UNIQUE(" + Contact.ACCOUNT + ", " + Contact.JID + ") ON CONFLICT REPLACE);"; + private static String CREATE_DISCOVERY_RESULTS_STATEMENT = "create table " + + ServiceDiscoveryResult.TABLENAME + "(" + + ServiceDiscoveryResult.HASH + " TEXT, " + + ServiceDiscoveryResult.VER + " TEXT, " + + ServiceDiscoveryResult.RESULT + " TEXT, " + + "UNIQUE(" + ServiceDiscoveryResult.HASH + ", " + + ServiceDiscoveryResult.VER + ") ON CONFLICT REPLACE);"; + private static String CREATE_PREKEYS_STATEMENT = "CREATE TABLE " + SQLiteAxolotlStore.PREKEY_TABLENAME + "(" + SQLiteAxolotlStore.ACCOUNT + " TEXT, " @@ -158,6 +168,7 @@ public class DatabaseBackend extends SQLiteOpenHelper { + ") ON DELETE CASCADE);"); db.execSQL(CREATE_CONTATCS_STATEMENT); + db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); db.execSQL(CREATE_SESSIONS_STATEMENT); db.execSQL(CREATE_PREKEYS_STATEMENT); db.execSQL(CREATE_SIGNED_PREKEYS_STATEMENT); @@ -355,6 +366,10 @@ public class DatabaseBackend extends SQLiteOpenHelper { if (oldVersion < 22 && newVersion >= 22) { db.execSQL("ALTER TABLE " + SQLiteAxolotlStore.IDENTITIES_TABLENAME + " ADD COLUMN " + SQLiteAxolotlStore.CERTIFICATE); } + + if (oldVersion < 23 && newVersion >= 23) { + db.execSQL(CREATE_DISCOVERY_RESULTS_STATEMENT); + } } public static synchronized DatabaseBackend getInstance(Context context) { @@ -379,6 +394,30 @@ public class DatabaseBackend extends SQLiteOpenHelper { db.insert(Account.TABLENAME, null, account.getContentValues()); } + public void insertDiscoveryResult(ServiceDiscoveryResult result) { + SQLiteDatabase db = this.getWritableDatabase(); + db.insert(ServiceDiscoveryResult.TABLENAME, null, result.getContentValues()); + } + + public ServiceDiscoveryResult findDiscoveryResult(final String hash, final String ver) { + SQLiteDatabase db = this.getReadableDatabase(); + String[] selectionArgs = {hash, ver}; + Cursor cursor = db.query(ServiceDiscoveryResult.TABLENAME, null, + ServiceDiscoveryResult.HASH + "=? AND " + ServiceDiscoveryResult.VER + "=?", + selectionArgs, null, null, null); + if (cursor.getCount() == 0) + return null; + cursor.moveToFirst(); + + ServiceDiscoveryResult result = null; + try { + result = new ServiceDiscoveryResult(cursor); + } catch (JSONException e) { /* result is still null */ } + + cursor.close(); + return result; + } + public CopyOnWriteArrayList<Conversation> getConversations(int status) { CopyOnWriteArrayList<Conversation> list = new CopyOnWriteArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 4403f99cd..165c7c2a8 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -24,13 +24,13 @@ 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>(); + private final HashSet<Query> queries = new HashSet<>(); + private final ArrayList<Query> pendingQueries = new ArrayList<>(); public enum PagingOrder { NORMAL, REVERSE - }; + } public MessageArchiveService(final XmppConnectionService service) { this.mXmppConnectionService = service; @@ -102,6 +102,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return null; } final Query query = new Query(conversation, start, end,PagingOrder.REVERSE); + query.reference = conversation.getFirstMamReference(); this.queries.add(query); this.execute(query); return query; @@ -136,12 +137,12 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { synchronized (MessageArchiveService.this.queries) { MessageArchiveService.this.queries.remove(query); if (query.hasCallback()) { - query.callback(); + query.callback(false); } } } else if (packet.getType() != IqPacket.TYPE.RESULT) { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": error executing mam: " + packet.toString()); - finalizeQuery(query); + finalizeQuery(query, true); } } }); @@ -152,14 +153,14 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } - private void finalizeQuery(Query query) { + private void finalizeQuery(Query query, boolean done) { synchronized (this.queries) { this.queries.remove(query); } final Conversation conversation = query.getConversation(); if (conversation != null) { conversation.sort(); - conversation.setHasMessagesLeftOnServer(query.getMessageCount() > 0); + conversation.setHasMessagesLeftOnServer(!done); } else { for(Conversation tmp : this.mXmppConnectionService.getConversations()) { if (tmp.getAccount() == query.getAccount()) { @@ -168,7 +169,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } if (query.hasCallback()) { - query.callback(); + query.callback(done); } else { this.mXmppConnectionService.updateConversationUi(); } @@ -202,9 +203,13 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { 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 (query.getConversation() != null) { + query.getConversation().setFirstMamReference(first == null ? null : first.getContent()); + } if (complete || relevant == null || abort) { - this.finalizeQuery(query); - Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid().toString()+": finished mam after "+query.getTotalCount()+" messages"); + final boolean done = (complete || query.getMessageCount() == 0) && query.getStart() == 0; + this.finalizeQuery(query, done); + Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done)); if (query.getWith() == null && query.getMessageCount() > 0) { mXmppConnectionService.getNotificationService().finishBacklog(true); } @@ -216,7 +221,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { nextQuery = query.prev(first == null ? null : first.getContent()); } this.execute(nextQuery); - this.finalizeQuery(query); + this.finalizeQuery(query, false); synchronized (this.queries) { this.queries.remove(query); this.queries.add(nextQuery); @@ -324,10 +329,10 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.callback = callback; } - public void callback() { + public void callback(boolean done) { if (this.callback != null) { this.callback.onMoreMessagesLoaded(messageCount,conversation); - if (messageCount == 0) { + if (done) { this.callback.informUser(R.string.no_more_history_on_server); } } @@ -345,12 +350,9 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return this.account; } - public void incrementTotalCount() { - this.totalCount++; - } - public void incrementMessageCount() { this.messageCount++; + this.totalCount++; } public int getTotalCount() { @@ -373,7 +375,8 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { public String toString() { StringBuilder builder = new StringBuilder(); if (this.muc()) { - builder.append("to="+this.getWith().toString()); + builder.append("to="); + builder.append(this.getWith().toString()); } else { builder.append("with="); if (this.getWith() == null) { diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index d4fda1b86..4fa5ddfff 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -72,7 +72,10 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; import eu.siacs.conversations.entities.MucOptions.OnRenameListener; +import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.Presences; +import eu.siacs.conversations.entities.Roster; +import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; import eu.siacs.conversations.generator.IqGenerator; @@ -244,6 +247,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa private OnKeyStatusUpdated mOnKeyStatusUpdated = null; private int keyStatusUpdatedListenerCount = 0; private SecureRandom mRandom; + private LruCache<Pair<String,String>,ServiceDiscoveryResult> discoCache = new LruCache<>(20); private final OnBindListener mOnBindListener = new OnBindListener() { @Override @@ -334,7 +338,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa } public PgpEngine getPgpEngine() { - if (pgpServiceConnection.isBound()) { + if (pgpServiceConnection != null && pgpServiceConnection.isBound()) { if (this.mPgpEngine == null) { this.mPgpEngine = new PgpEngine(new OpenPgpApi( getApplicationContext(), @@ -596,13 +600,13 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return getPreferences().getString("picture_compression", "auto"); } - private int getTargetPresence() { + private Presence.Status getTargetPresence() { if (xaOnSilentMode() && isPhoneSilenced()) { - return Presences.XA; + return Presence.Status.XA; } else if (awayWhenScreenOff() && !isInteractive()) { - return Presences.AWAY; + return Presence.Status.AWAY; } else { - return Presences.ONLINE; + return Presence.Status.ONLINE; } } @@ -1001,6 +1005,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa final Element query = packet.query(); final HashMap<Jid, Bookmark> bookmarks = new HashMap<>(); final Element storage = query.findChild("storage", "storage:bookmarks"); + final boolean autojoin = respectAutojoin(); if (storage != null) { for (final Element item : storage.getChildren()) { if (item.getName().equals("conference")) { @@ -1012,7 +1017,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa Conversation conversation = find(bookmark); if (conversation != null) { conversation.setBookmark(bookmark); - } else if (bookmark.autojoin() && bookmark.getJid() != null) { + } else if (bookmark.autojoin() && bookmark.getJid() != null && autojoin) { conversation = findOrCreateConversation( account, bookmark.getJid(), true); conversation.setBookmark(bookmark); @@ -1210,6 +1215,8 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void loadMoreMessages(final Conversation conversation, final long timestamp, final OnMoreMessagesLoaded callback) { if (XmppConnectionService.this.getMessageArchiveService().queryInProgress(conversation, callback)) { return; + } else if (timestamp == 0) { + return; } Log.d(Config.LOGTAG, "load more messages for " + conversation.getName() + " prior to " + MessageGenerator.getTimestamp(timestamp)); Runnable runnable = new Runnable() { @@ -1222,10 +1229,11 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa checkDeletedFiles(conversation); callback.onMoreMessagesLoaded(messages.size(), conversation); } else if (conversation.hasMessagesLeftOnServer() - && account.isOnlineAndConnected()) { + && account.isOnlineAndConnected() + && conversation.getLastClearHistory() == 0) { if ((conversation.getMode() == Conversation.MODE_SINGLE && account.getXmppConnection().getFeatures().mam()) || (conversation.getMode() == Conversation.MODE_MULTI && conversation.getMucOptions().mamSupport())) { - MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp - 1); + MessageArchiveService.Query query = getMessageArchiveService().query(conversation, 0, timestamp); if (query != null) { query.setCallback(callback); } @@ -1330,7 +1338,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (conversation.getMode() == Conversation.MODE_MULTI) { if (conversation.getAccount().getStatus() == Account.State.ONLINE) { Bookmark bookmark = conversation.getBookmark(); - if (bookmark != null && bookmark.autojoin()) { + if (bookmark != null && bookmark.autojoin() && respectAutojoin()) { bookmark.setAutojoin(false); pushBookmarks(bookmark.getAccount()); } @@ -1791,7 +1799,9 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa if (conversation.getMode() == Conversation.MODE_MULTI) { conversation.getMucOptions().setPassword(password); if (conversation.getBookmark() != null) { - conversation.getBookmark().setAutojoin(true); + if (respectAutojoin()) { + conversation.getBookmark().setAutojoin(true); + } pushBookmarks(conversation.getAccount()); } databaseBackend.updateConversation(conversation); @@ -2578,6 +2588,10 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa return !getPreferences().getBoolean("dont_save_encrypted", false); } + private boolean respectAutojoin() { + return getPreferences().getBoolean("autojoin", true); + } + public boolean indicateReceived() { return getPreferences().getBoolean("indicate_received", false); } @@ -2900,6 +2914,7 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa public void clearConversationHistory(final Conversation conversation) { conversation.clearMessages(); conversation.setHasMessagesLeftOnServer(false); //avoid messages getting loaded through mam + conversation.setLastClearHistory(System.currentTimeMillis()); Runnable runnable = new Runnable() { @Override public void run() { @@ -2948,13 +2963,67 @@ public class XmppConnectionService extends Service implements OnPhoneContactsLoa @Override public void onIqPacketReceived(Account account, IqPacket packet) { if (packet.getType() == IqPacket.TYPE.ERROR) { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": could not publish nick"); + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": could not publish nick"); } } }); } } + private ServiceDiscoveryResult getCachedServiceDiscoveryResult(Pair<String,String> key) { + ServiceDiscoveryResult result = discoCache.get(key); + if (result != null) { + return result; + } else { + result = databaseBackend.findDiscoveryResult(key.first, key.second); + if (result != null) { + discoCache.put(key, result); + } + return result; + } + } + + public void fetchCaps(Account account, final Jid jid, final Presence presence) { + final Pair<String,String> key = new Pair<>(presence.getHash(), presence.getVer()); + ServiceDiscoveryResult disco = getCachedServiceDiscoveryResult(key); + if (disco != null) { + presence.setServiceDiscoveryResult(disco); + } else { + if (!account.inProgressDiscoFetches.contains(key)) { + account.inProgressDiscoFetches.add(key); + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(jid); + request.query("http://jabber.org/protocol/disco#info"); + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": making disco request for "+key.second+" to "+jid); + sendIqPacket(account, request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket discoPacket) { + if (discoPacket.getType() == IqPacket.TYPE.RESULT) { + ServiceDiscoveryResult disco = new ServiceDiscoveryResult(discoPacket); + if (presence.getVer().equals(disco.getVer())) { + databaseBackend.insertDiscoveryResult(disco); + injectServiceDiscorveryResult(account.getRoster(), presence.getHash(), presence.getVer(), disco); + } else { + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": mismatch in caps for contact " + jid+" "+presence.getVer()+" vs "+disco.getVer()); + } + } + account.inProgressDiscoFetches.remove(key); + } + }); + } + } + } + + private void injectServiceDiscorveryResult(Roster roster, String hash, String ver, ServiceDiscoveryResult disco) { + for(Contact contact : roster.getContacts()) { + for(Presence presence : contact.getPresences().getPresences().values()) { + if (hash.equals(presence.getHash()) && ver.equals(presence.getVer())) { + presence.setServiceDiscoveryResult(disco); + } + } + } + } + public interface OnAccountCreated { void onAccountCreated(Account account); diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 6653dd24c..be4549369 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -468,7 +468,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers bookmark.setNick(mConversation.getJid().getResourcepart()); } bookmark.setBookmarkName(mConversation.getMucOptions().getSubject()); - bookmark.setAutojoin(true); + bookmark.setAutojoin(getPreferences().getBoolean("autojoin",true)); account.getBookmarks().add(bookmark); xmppConnectionService.pushBookmarks(account); mConversation.setBookmark(bookmark); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index dcb146534..7fc40860c 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -948,7 +948,7 @@ public class ConversationActivity extends XmppActivity MenuItem pgp = popup.getMenu().findItem(R.id.encryption_choice_pgp); MenuItem axolotl = popup.getMenu().findItem(R.id.encryption_choice_axolotl); pgp.setVisible(!Config.HIDE_PGP_IN_UI && !Config.X509_VERIFICATION); - none.setVisible(!Config.FORCE_E2E_ENCRYPTION); + none.setVisible(!Config.FORCE_E2E_ENCRYPTION || conversation.getMode() == Conversation.MODE_MULTI); otr.setVisible(!Config.X509_VERIFICATION); if (conversation.getMode() == Conversation.MODE_MULTI) { otr.setVisible(false); @@ -1698,18 +1698,24 @@ public class ConversationActivity extends XmppActivity return !isConversationsOverviewHideable() || this.conversationWasSelectedByKeyboard; } - @Override - public void onClick(View view) { - final Conversation conversation = getSelectedConversation(); - Log.e("Con","Clicked Title"); - if (conversation.getMode() == Conversation.MODE_SINGLE) { - switchToContactDetails(getSelectedConversation().getContact()); - } else if (conversation.getMode() == Conversation.MODE_MULTI) { - Intent intent = new Intent(this, - ConferenceDetailsActivity.class); - intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); - intent.putExtra("uuid", getSelectedConversation().getUuid()); - startActivity(intent); - } - } -}
\ No newline at end of file + @Override + public void onClick(View view) { + final Conversation conversation = getSelectedConversation(); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + switchToContactDetails(getSelectedConversation().getContact()); + } else if (conversation.getMode() == Conversation.MODE_MULTI) { + Intent intent = new Intent(this, + ConferenceDetailsActivity.class); + intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); + intent.putExtra("uuid", getSelectedConversation().getUuid()); + startActivity(intent); + } + } +} + + public void setMessagesLoaded() { + if (mConversationFragment != null) { + mConversationFragment.setMessagesLoaded(); + } + } +} diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index eb4dcd4f0..ddb3c6f84 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -40,12 +40,9 @@ import net.java.otr4j.session.SessionStatus; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.NoSuchElementException; -import java.util.concurrent.ConcurrentLinkedQueue; import eu.siacs.conversations.Config; import eu.siacs.conversations.R; -import eu.siacs.conversations.crypto.PgpEngine; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Contact; @@ -53,6 +50,7 @@ import eu.siacs.conversations.entities.Conversation; import eu.siacs.conversations.entities.DownloadableFile; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.MucOptions; +import eu.siacs.conversations.entities.Presence; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; @@ -148,7 +146,12 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa int visibleItemCount, int totalItemCount) { synchronized (ConversationFragment.this.messageList) { if (firstVisibleItem < 5 && messagesLoaded && messageList.size() > 0) { - long timestamp = ConversationFragment.this.messageList.get(0).getTimeSent(); + long timestamp; + if (messageList.get(0).getType() == Message.TYPE_STATUS && messageList.size() >= 2) { + timestamp = messageList.get(1).getTimeSent(); + } else { + timestamp = messageList.get(0).getTimeSent(); + } messagesLoaded = false; activity.xmppConnectionService.loadMoreMessages(conversation, timestamp, new XmppConnectionService.OnMoreMessagesLoaded() { @Override @@ -317,6 +320,10 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa private ConversationActivity activity; private Message selectedMessage; + public void setMessagesLoaded() { + this.messagesLoaded = true; + } + private void sendMessage() { final String body = mEditMessage.getText().toString(); if (body.length() == 0 || this.conversation == null) { @@ -769,7 +776,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case MucOptions.ERROR_NICK_IN_USE: showSnackbar(R.string.nick_in_use, R.string.edit, clickToMuc); break; - case MucOptions.ERROR_UNKNOWN: + case MucOptions.ERROR_NO_RESPONSE: showSnackbar(R.string.conference_not_found, R.string.leave, leaveMuc); break; case MucOptions.ERROR_PASSWORD_REQUIRED: @@ -784,6 +791,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa case MucOptions.KICKED_FROM_ROOM: showSnackbar(R.string.conference_kicked, R.string.join, joinMuc); break; + case MucOptions.ERROR_UNKNOWN: + showSnackbar(R.string.conference_unknown_error, R.string.try_again, joinMuc); + break; default: break; } @@ -859,82 +869,82 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa enum SendButtonAction {TEXT, TAKE_PHOTO, SEND_LOCATION, RECORD_VOICE, CANCEL, CHOOSE_PICTURE} - private int getSendButtonImageResource(SendButtonAction action, int status) { + private int getSendButtonImageResource(SendButtonAction action, Presence.Status status) { switch (action) { case TEXT: switch (status) { - case Presences.CHAT: - case Presences.ONLINE: + case CHAT: + case ONLINE: return R.drawable.ic_send_text_online; - case Presences.AWAY: + case AWAY: return R.drawable.ic_send_text_away; - case Presences.XA: - case Presences.DND: + case XA: + case DND: return R.drawable.ic_send_text_dnd; default: return R.drawable.ic_send_text_offline; } case TAKE_PHOTO: switch (status) { - case Presences.CHAT: - case Presences.ONLINE: + case CHAT: + case ONLINE: return R.drawable.ic_send_photo_online; - case Presences.AWAY: + case AWAY: return R.drawable.ic_send_photo_away; - case Presences.XA: - case Presences.DND: + case XA: + case DND: return R.drawable.ic_send_photo_dnd; default: return R.drawable.ic_send_photo_offline; } case RECORD_VOICE: switch (status) { - case Presences.CHAT: - case Presences.ONLINE: + case CHAT: + case ONLINE: return R.drawable.ic_send_voice_online; - case Presences.AWAY: + case AWAY: return R.drawable.ic_send_voice_away; - case Presences.XA: - case Presences.DND: + case XA: + case DND: return R.drawable.ic_send_voice_dnd; default: return R.drawable.ic_send_voice_offline; } case SEND_LOCATION: switch (status) { - case Presences.CHAT: - case Presences.ONLINE: + case CHAT: + case ONLINE: return R.drawable.ic_send_location_online; - case Presences.AWAY: + case AWAY: return R.drawable.ic_send_location_away; - case Presences.XA: - case Presences.DND: + case XA: + case DND: return R.drawable.ic_send_location_dnd; default: return R.drawable.ic_send_location_offline; } case CANCEL: switch (status) { - case Presences.CHAT: - case Presences.ONLINE: + case CHAT: + case ONLINE: return R.drawable.ic_send_cancel_online; - case Presences.AWAY: + case AWAY: return R.drawable.ic_send_cancel_away; - case Presences.XA: - case Presences.DND: + case XA: + case DND: return R.drawable.ic_send_cancel_dnd; default: return R.drawable.ic_send_cancel_offline; } case CHOOSE_PICTURE: switch (status) { - case Presences.CHAT: - case Presences.ONLINE: + case CHAT: + case ONLINE: return R.drawable.ic_send_picture_online; - case Presences.AWAY: + case AWAY: return R.drawable.ic_send_picture_away; - case Presences.XA: - case Presences.DND: + case XA: + case DND: return R.drawable.ic_send_picture_dnd; default: return R.drawable.ic_send_picture_offline; @@ -946,7 +956,7 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa public void updateSendButton() { final Conversation c = this.conversation; final SendButtonAction action; - final int status; + final Presence.Status status; final boolean empty = this.mEditMessage == null || this.mEditMessage.getText().length() == 0; final boolean conference = c.getMode() == Conversation.MODE_MULTI; if (conference && !c.getAccount().httpUploadAvailable()) { @@ -993,10 +1003,10 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa if (c.getMode() == Conversation.MODE_SINGLE) { status = c.getContact().getMostAvailableStatus(); } else { - status = c.getMucOptions().online() ? Presences.ONLINE : Presences.OFFLINE; + status = c.getMucOptions().online() ? Presence.Status.ONLINE : Presence.Status.OFFLINE; } } else { - status = Presences.OFFLINE; + status = Presence.Status.OFFLINE; } this.mSendButton.setTag(action); this.mSendButton.setImageResource(getSendButtonImageResource(action, status)); @@ -1004,6 +1014,9 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa protected void updateStatusMessages() { synchronized (this.messageList) { + if (conversation.getLastClearHistory() != 0) { + this.messageList.add(0, Message.createLoadMoreMessage(conversation)); + } if (conversation.getMode() == Conversation.MODE_SINGLE) { ChatState state = conversation.getIncomingChatState(); if (state == ChatState.COMPOSING) { diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 00a501f39..8b3b4607b 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java @@ -63,6 +63,7 @@ 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.entities.Presence; import eu.siacs.conversations.entities.Presences; import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate; import eu.siacs.conversations.ui.adapter.KnownHostsAdapter; @@ -286,7 +287,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU if (!conversation.getMucOptions().online()) { xmppConnectionService.joinMuc(conversation); } - if (!bookmark.autojoin()) { + if (!bookmark.autojoin() && getPreferences().getBoolean("autojoin", true)) { bookmark.setAutojoin(true); xmppConnectionService.pushBookmarks(bookmark.getAccount()); } @@ -426,7 +427,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU jid.setError(getString(R.string.bookmark_already_exists)); } else { final Bookmark bookmark = new Bookmark(account, conferenceJid.toBareJid()); - bookmark.setAutojoin(true); + bookmark.setAutojoin(getPreferences().getBoolean("autojoin", true)); String nick = conferenceJid.getResourcepart(); if (nick != null && !nick.isEmpty()) { bookmark.setNick(nick); @@ -725,9 +726,11 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU for (Account account : xmppConnectionService.getAccounts()) { if (account.getStatus() != Account.State.DISABLED) { for (Contact contact : account.getRoster().getContacts()) { + Presence p = contact.getPresences().getMostAvailablePresence(); + Presence.Status s = p == null ? Presence.Status.OFFLINE : p.getStatus(); if (contact.showInRoster() && contact.match(needle) && (!this.mHideOfflineContacts - || contact.getPresences().getMostAvailableStatus() < Presences.OFFLINE)) { + || s.compareTo(Presence.Status.OFFLINE) < 0)) { this.contacts.add(contact); } } @@ -803,7 +806,7 @@ public class StartConversationActivity extends XmppActivity implements OnRosterU final AdapterView.AdapterContextMenuInfo acmi = (AdapterContextMenuInfo) menuInfo; if (mResContextMenu == R.menu.conference_context) { activity.conference_context_id = acmi.position; - } else { + } else if (mResContextMenu == R.menu.contact_context){ 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); 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 780094585..7755ef08c 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -430,6 +430,19 @@ public class MessageAdapter extends ArrayAdapter<Message> { viewHolder.image.setOnLongClickListener(openContextMenu); } + private void loadMoreMessages(Conversation conversation) { + conversation.setLastClearHistory(0); + conversation.setHasMessagesLeftOnServer(true); + conversation.setFirstMamReference(null); + long timestamp = conversation.getLastMessageTransmitted(); + if (timestamp == 0) { + timestamp = System.currentTimeMillis(); + } + activity.setMessagesLoaded(); + activity.xmppConnectionService.getMessageArchiveService().query(conversation, 0, timestamp); + Toast.makeText(activity, R.string.fetching_history_from_server,Toast.LENGTH_LONG).show(); + } + @Override public View getView(int position, View view, ViewGroup parent) { final Message message = getItem(position); @@ -488,6 +501,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { view = activity.getLayoutInflater().inflate(R.layout.message_status, parent, false); viewHolder.contact_picture = (ImageView) view.findViewById(R.id.message_photo); viewHolder.status_message = (TextView) view.findViewById(R.id.status_message); + viewHolder.load_more_messages = (Button) view.findViewById(R.id.load_more_messages); break; default: viewHolder = null; @@ -504,16 +518,31 @@ public class MessageAdapter extends ArrayAdapter<Message> { boolean darkBackground = (type == RECEIVED && (!isInValidSession || !mUseWhiteBackground)); if (type == STATUS) { - if (conversation.getMode() == Conversation.MODE_SINGLE) { - viewHolder.contact_picture.setImageBitmap(activity - .avatarService().get(conversation.getContact(), - activity.getPixel(32))); - viewHolder.contact_picture.setAlpha(0.5f); + if ("LOAD_MORE".equals(message.getBody())) { + viewHolder.status_message.setVisibility(View.GONE); + viewHolder.contact_picture.setVisibility(View.GONE); + viewHolder.load_more_messages.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + loadMoreMessages(message.getConversation()); + } + }); + } else { + viewHolder.status_message.setVisibility(View.VISIBLE); + viewHolder.contact_picture.setVisibility(View.VISIBLE); + viewHolder.load_more_messages.setVisibility(View.GONE); + if (conversation.getMode() == Conversation.MODE_SINGLE) { + viewHolder.contact_picture.setImageBitmap(activity + .avatarService().get(conversation.getContact(), + activity.getPixel(32))); + viewHolder.contact_picture.setAlpha(0.5f); + } viewHolder.status_message.setText(message.getBody()); } return view; } else { - loadAvatar(message,viewHolder.contact_picture); + loadAvatar(message, viewHolder.contact_picture); } viewHolder.contact_picture @@ -676,6 +705,7 @@ public class MessageAdapter extends ArrayAdapter<Message> { protected ImageView contact_picture; protected TextView status_message; protected TextView encryption; + public Button load_more_messages; } class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> { diff --git a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java index 87790d64d..306d50c29 100644 --- a/src/main/java/eu/siacs/conversations/utils/DNSHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/DNSHelper.java @@ -5,6 +5,7 @@ import android.content.Context; import android.net.ConnectivityManager; import android.net.LinkProperties; import android.net.Network; +import android.net.RouteInfo; import android.os.Build; import android.os.Bundle; import android.os.Parcelable; @@ -75,15 +76,29 @@ public class DNSHelper { for(int i = 0; i < networks.length; ++i) { LinkProperties linkProperties = connectivityManager.getLinkProperties(networks[i]); if (linkProperties != null) { - servers.addAll(linkProperties.getDnsServers()); + if (hasDefaultRoute(linkProperties)) { + servers.addAll(0, linkProperties.getDnsServers()); + } else { + servers.addAll(linkProperties.getDnsServers()); + } } } if (servers.size() > 0) { - Log.d(Config.LOGTAG,"used lollipop variant to discover dns servers in "+networks.length+" networks"); + Log.d(Config.LOGTAG, "used lollipop variant to discover dns servers in " + networks.length + " networks"); } return servers.size() > 0 ? servers : getDnsServersPreLollipop(); } + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + private static boolean hasDefaultRoute(LinkProperties linkProperties) { + for(RouteInfo route: linkProperties.getRoutes()) { + if (route.isDefaultRoute()) { + return true; + } + } + return false; + } + private static List<InetAddress> getDnsServersPreLollipop() { List<InetAddress> servers = new ArrayList<>(); String[] dns = client.findDNS(); diff --git a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java b/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java index 49e9a81ad..3a8c1c0aa 100644 --- a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java @@ -1,11 +1,14 @@ package eu.siacs.conversations.utils; +import android.os.Build; + import java.lang.reflect.Method; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; +import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; @@ -59,4 +62,12 @@ public class SSLSocketHelper { // ignore any error, we just can't set the alpn protocol... } } + + public static SSLContext getSSLContext() throws NoSuchAlgorithmException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + return SSLContext.getInstance("TLSv1.2"); + } else { + return SSLContext.getInstance("TLS"); + } + } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index cc2579a9a..7f987ae9f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -58,6 +58,7 @@ import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.ScramSha1; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; +import eu.siacs.conversations.entities.ServiceDiscoveryResult; import eu.siacs.conversations.generator.IqGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.CryptoHelper; @@ -101,7 +102,7 @@ public class XmppConnection implements Runnable { private boolean needsBinding = true; private boolean shouldAuthenticate = true; private Element streamFeatures; - private final HashMap<Jid, Info> disco = new HashMap<>(); + private final HashMap<Jid, ServiceDiscoveryResult> disco = new HashMap<>(); private String streamId = null; private int smVersion = 3; @@ -407,7 +408,7 @@ public class XmppConnection implements Runnable { } private TlsFactoryVerifier getTlsFactoryVerifier() throws NoSuchAlgorithmException, KeyManagementException, IOException { - final SSLContext sc = SSLContext.getInstance("TLS"); + final SSLContext sc = SSLSocketHelper.getSSLContext(); MemorizingTrustManager trustManager = this.mXmppConnectionService.getMemorizingTrustManager(); KeyManager[] keyManager; if (account.getPrivateKeyAlias() != null && account.getPassword().isEmpty()) { @@ -1021,7 +1022,19 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery"); mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); sendServiceDiscoveryItems(account.getServer()); - sendServiceDiscoveryInfo(account.getServer()); + Element caps = streamFeatures.findChild("c"); + final String hash = caps == null ? null : caps.getAttribute("hash"); + final String ver = caps == null ? null : caps.getAttribute("ver"); + ServiceDiscoveryResult discoveryResult = null; + if (hash != null && ver != null) { + discoveryResult = mXmppConnectionService.databaseBackend.findDiscoveryResult(hash, ver); + } + if (discoveryResult == null) { + sendServiceDiscoveryInfo(account.getServer()); + } else { + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server caps came from cache"); + disco.put(account.getServer(), discoveryResult); + } sendServiceDiscoveryInfo(account.getJid().toBareJid()); this.lastSessionStarted = SystemClock.elapsedRealtime(); } @@ -1040,39 +1053,29 @@ public class XmppConnection implements Runnable { if (packet.getType() == IqPacket.TYPE.RESULT) { boolean advancedStreamFeaturesLoaded; synchronized (XmppConnection.this.disco) { - final List<Element> elements = packet.query().getChildren(); - final Info info = new Info(); - for (final Element element : elements) { - if (element.getName().equals("identity")) { - String type = element.getAttribute("type"); - String category = element.getAttribute("category"); - String name = element.getAttribute("name"); - if (type != null && category != null) { - info.identities.add(new Pair<>(category, type)); - if (mServerIdentity == Identity.UNKNOWN - && type.equals("im") - && category.equals("server")) { - if (name != null && jid.equals(account.getServer())) { - switch (name) { - case "Prosody": - mServerIdentity = Identity.PROSODY; - break; - case "ejabberd": - mServerIdentity = Identity.EJABBERD; - break; - case "Slack-XMPP": - mServerIdentity = Identity.SLACK; - break; - } - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server name: " + name); - } + ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); + for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) { + if (mServerIdentity == Identity.UNKNOWN && id.getType().equals("im") && + id.getCategory().equals("server") && id.getName() != null && + jid.equals(account.getServer())) { + switch (id.getName()) { + case "Prosody": + mServerIdentity = Identity.PROSODY; + break; + case "ejabberd": + mServerIdentity = Identity.EJABBERD; + break; + case "Slack-XMPP": + mServerIdentity = Identity.SLACK; + break; } + Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server name: " + id.getName()); } - } else if (element.getName().equals("feature")) { - info.features.add(element.getAttribute("var")); - } } - disco.put(jid, info); + if (jid.equals(account.getServer())) { + mXmppConnectionService.databaseBackend.insertDiscoveryResult(result); + } + disco.put(jid, result); advancedStreamFeaturesLoaded = disco.containsKey(account.getServer()) && disco.containsKey(account.getJid().toBareJid()); } @@ -1324,8 +1327,8 @@ public class XmppConnection implements Runnable { public List<Jid> findDiscoItemsByFeature(final String feature) { synchronized (this.disco) { final List<Jid> items = new ArrayList<>(); - for (final Entry<Jid, Info> cursor : this.disco.entrySet()) { - if (cursor.getValue().features.contains(feature)) { + for (final Entry<Jid, ServiceDiscoveryResult> cursor : this.disco.entrySet()) { + if (cursor.getValue().getFeatures().contains(feature)) { items.add(cursor.getKey()); } } @@ -1352,11 +1355,11 @@ public class XmppConnection implements Runnable { public String getMucServer() { synchronized (this.disco) { - for (final Entry<Jid, Info> cursor : disco.entrySet()) { - final Info value = cursor.getValue(); - if (value.features.contains("http://jabber.org/protocol/muc") - && !value.features.contains("jabber:iq:gateway") - && !value.identities.contains(new Pair<>("conference", "irc"))) { + for (final Entry<Jid, ServiceDiscoveryResult> cursor : disco.entrySet()) { + final ServiceDiscoveryResult value = cursor.getValue(); + if (value.getFeatures().contains("http://jabber.org/protocol/muc") + && !value.getFeatures().contains("jabber:iq:gateway") + && !value.hasIdentity("conference", "irc")) { return cursor.getKey().toString(); } } @@ -1419,11 +1422,6 @@ public class XmppConnection implements Runnable { return mServerIdentity; } - private class Info { - public final ArrayList<String> features = new ArrayList<>(); - public final ArrayList<Pair<String,String>> identities = new ArrayList<>(); - } - private class UnauthorizedException extends IOException { } @@ -1458,7 +1456,7 @@ public class XmppConnection implements Runnable { private boolean hasDiscoFeature(final Jid server, final String feature) { synchronized (XmppConnection.this.disco) { return connection.disco.containsKey(server) && - connection.disco.get(server).features.contains(feature); + connection.disco.get(server).getFeatures().contains(feature); } } @@ -1486,12 +1484,12 @@ public class XmppConnection implements Runnable { public boolean pep() { synchronized (XmppConnection.this.disco) { final Pair<String, String> needle = new Pair<>("pubsub", "pep"); - Info info = disco.get(account.getServer()); - if (info != null && info.identities.contains(needle)) { + ServiceDiscoveryResult info = disco.get(account.getServer()); + if (info != null && info.hasIdentity("pubsub", "pep")) { return true; } else { info = disco.get(account.getJid().toBareJid()); - return info != null && info.identities.contains(needle); + return info != null && info.hasIdentity("pubsub", "pep"); } } } 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 0053a399c..50a418921 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java +++ b/src/main/java/eu/siacs/conversations/xmpp/forms/Data.java @@ -80,7 +80,8 @@ public class Data extends Element { } public String getFormType() { - return this.getAttribute("FORM_TYPE"); + Field typeFiled = this.getFieldByName("FORM_TYPE"); + return typeFiled == null ? "" : typeFiled.getValue(); } public String getTitle() { |