From fccce229c6a75e3a0b4845199323a00cadf21ac7 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 10 Jan 2016 14:56:55 -0500 Subject: [PATCH 1/7] Factor out a representation of XEP-0030 results And the parser from Element to this representation. --- .../entities/ServiceDiscoveryResult.java | 84 +++++++++++++++++++ .../conversations/xmpp/XmppConnection.java | 77 +++++++---------- 2 files changed, 114 insertions(+), 47 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java 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..963464402 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -0,0 +1,84 @@ +package eu.siacs.conversations.entities; + +import java.util.List; +import java.util.ArrayList; + +import eu.siacs.conversations.xml.Element; +import eu.siacs.conversations.xmpp.stanzas.IqPacket; + +public class ServiceDiscoveryResult { + public static class Identity { + protected final String category; + protected final String type; + protected final String name; + + public Identity(final String category, final String type, final String name) { + this.category = category; + this.type = type; + this.name = name; + } + + public Identity(final Element el) { + this.category = el.getAttribute("category"); + this.type = el.getAttribute("type"); + this.name = el.getAttribute("name"); + } + + public String getCategory() { + return this.category; + } + + public String getType() { + return this.type; + } + + public String getName() { + return this.name; + } + } + + protected final List identities; + protected final List features; + + public ServiceDiscoveryResult(final List identities, final List features) { + this.identities = identities; + this.features = features; + } + + public ServiceDiscoveryResult(final IqPacket packet) { + this.identities = new ArrayList<>(); + this.features = new ArrayList<>(); + + final List 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")) { + features.add(element.getAttribute("var")); + } + } + } + + public List getIdentities() { + return this.identities; + } + + public List 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; + } +} diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index a3dd06446..556688fd8 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 disco = new HashMap<>(); + private final HashMap disco = new HashMap<>(); private String streamId = null; private int smVersion = 3; @@ -1031,39 +1032,26 @@ public class XmppConnection implements Runnable { if (packet.getType() == IqPacket.TYPE.RESULT) { boolean advancedStreamFeaturesLoaded; synchronized (XmppConnection.this.disco) { - final List 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); + disco.put(jid, result); advancedStreamFeaturesLoaded = disco.containsKey(account.getServer()) && disco.containsKey(account.getJid().toBareJid()); } @@ -1316,8 +1304,8 @@ public class XmppConnection implements Runnable { public List findDiscoItemsByFeature(final String feature) { synchronized (this.disco) { final List items = new ArrayList<>(); - for (final Entry cursor : this.disco.entrySet()) { - if (cursor.getValue().features.contains(feature)) { + for (final Entry cursor : this.disco.entrySet()) { + if (cursor.getValue().getFeatures().contains(feature)) { items.add(cursor.getKey()); } } @@ -1344,11 +1332,11 @@ public class XmppConnection implements Runnable { public String getMucServer() { synchronized (this.disco) { - for (final Entry 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 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(); } } @@ -1411,11 +1399,6 @@ public class XmppConnection implements Runnable { return mServerIdentity; } - private class Info { - public final ArrayList features = new ArrayList<>(); - public final ArrayList> identities = new ArrayList<>(); - } - private class UnauthorizedException extends IOException { } @@ -1450,7 +1433,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); } } @@ -1478,12 +1461,12 @@ public class XmppConnection implements Runnable { public boolean pep() { synchronized (XmppConnection.this.disco) { final Pair 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"); } } } From 1e335d527b888963aa953542cfae866ade0d5867 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 10 Jan 2016 16:25:26 -0500 Subject: [PATCH 2/7] Generate capHash from any discovery result --- .../entities/ServiceDiscoveryResult.java | 84 ++++++++++++++++++- 1 file changed, 80 insertions(+), 4 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index 963464402..853e75aac 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -1,26 +1,41 @@ package eu.siacs.conversations.entities; -import java.util.List; +import android.content.ContentValues; +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.List; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.stanzas.IqPacket; public class ServiceDiscoveryResult { - public static class Identity { + + 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 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.category = el.getAttribute("category"); this.type = el.getAttribute("type"); + this.lang = el.getAttribute("xml:lang"); this.name = el.getAttribute("name"); } @@ -32,9 +47,29 @@ public class ServiceDiscoveryResult { 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; + } } protected final List identities; @@ -58,7 +93,9 @@ public class ServiceDiscoveryResult { identities.add(id); } } else if (element.getName().equals("feature")) { - features.add(element.getAttribute("var")); + if (element.getAttribute("var") != null) { + features.add(element.getAttribute("var")); + } } } } @@ -81,4 +118,43 @@ public class ServiceDiscoveryResult { return false; } + + public byte[] getCapHash() { + StringBuilder s = new StringBuilder(); + + List 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 features = this.getFeatures(); + Collections.sort(features); + + for (String feature : features) { + s.append(feature + "<"); + } + + // TODO: data forms? + + 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; + } + } + } From 56f8fff935baa4cec804807cfed3728d11c086ed Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 10 Jan 2016 16:43:48 -0500 Subject: [PATCH 3/7] Implement toJSON on ServiceDiscoveryResult --- .../entities/ServiceDiscoveryResult.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index 853e75aac..ac4e8c47e 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -9,6 +9,9 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Collections; 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.stanzas.IqPacket; @@ -70,6 +73,19 @@ public class ServiceDiscoveryResult { 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 List identities; @@ -157,4 +173,22 @@ public class ServiceDiscoveryResult { } } + 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; + } + } + } From ad36a4ba89f880a5522ffd5179dcaa98985e4164 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Tue, 12 Jan 2016 21:53:38 -0500 Subject: [PATCH 4/7] Persisitence and loading for ServiceDiscoveryResult --- .../entities/ServiceDiscoveryResult.java | 68 ++++++++++++++++--- .../persistance/DatabaseBackend.java | 41 ++++++++++- 2 files changed, 98 insertions(+), 11 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java index ac4e8c47e..9d77efab8 100644 --- a/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java +++ b/src/main/java/eu/siacs/conversations/entities/ServiceDiscoveryResult.java @@ -1,6 +1,7 @@ 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; @@ -17,6 +18,10 @@ import eu.siacs.conversations.xml.Element; 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; @@ -36,10 +41,21 @@ public class ServiceDiscoveryResult { } public Identity(final Element el) { - this.category = el.getAttribute("category"); - this.type = el.getAttribute("type"); - this.lang = el.getAttribute("xml:lang"); - this.name = el.getAttribute("name"); + 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() { @@ -88,17 +104,15 @@ public class ServiceDiscoveryResult { } } + protected final String hash; + protected final byte[] ver; protected final List identities; protected final List features; - public ServiceDiscoveryResult(final List identities, final List features) { - this.identities = identities; - this.features = features; - } - public ServiceDiscoveryResult(final IqPacket packet) { this.identities = new ArrayList<>(); this.features = new ArrayList<>(); + this.hash = "sha-1"; // We only support sha-1 for now final List elements = packet.query().getChildren(); @@ -114,6 +128,33 @@ public class ServiceDiscoveryResult { } } } + + this.ver = this.mkCapHash(); + } + + public ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException { + this.identities = new ArrayList<>(); + this.features = new ArrayList<>(); + this.hash = hash; + this.ver = ver; + + JSONArray identities = o.optJSONArray("identities"); + for(int i = 0; i < identities.length(); i++) { + this.identities.add(new Identity(identities.getJSONObject(i))); + } + + JSONArray features = o.optJSONArray("features"); + for(int i = 0; i < features.length(); i++) { + this.features.add(features.getString(i)); + } + } + + 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 getIdentities() { @@ -135,7 +176,7 @@ public class ServiceDiscoveryResult { return false; } - public byte[] getCapHash() { + protected byte[] mkCapHash() { StringBuilder s = new StringBuilder(); List identities = this.getIdentities(); @@ -191,4 +232,11 @@ public class ServiceDiscoveryResult { } } + public ContentValues getContentValues() { + final ContentValues values = new ContentValues(); + values.put(HASH, this.hash); + values.put(VER, new String(Base64.encode(this.ver, Base64.DEFAULT)).trim()); + values.put(RESULT, this.toJSON().toString()); + return values; + } } 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 getConversations(int status) { CopyOnWriteArrayList list = new CopyOnWriteArrayList<>(); SQLiteDatabase db = this.getReadableDatabase(); From bf5b2f73f5a61f0a0179c9d4431579f87eecb001 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 17 Jan 2016 16:28:38 -0500 Subject: [PATCH 5/7] Use a Presence class for presence information Only has status for now, but doing it so I can add disco to it --- .../siacs/conversations/entities/Contact.java | 23 ++++--- .../conversations/entities/Presence.java | 49 +++++++++++++ .../conversations/entities/Presences.java | 46 +++---------- .../generator/PresenceGenerator.java | 19 ++--- .../conversations/parser/PresenceParser.java | 3 +- .../services/XmppConnectionService.java | 9 +-- .../ui/ConversationFragment.java | 69 ++++++++++--------- .../ui/StartConversationActivity.java | 5 +- 8 files changed, 121 insertions(+), 102 deletions(-) create mode 100644 src/main/java/eu/siacs/conversations/entities/Presence.java diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index c2d8b278b..ed773b6e6 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -135,17 +135,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; } @@ -225,8 +225,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) { @@ -238,8 +238,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 boolean setPhotoUri(String uri) { 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..adba74d12 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/entities/Presence.java @@ -0,0 +1,49 @@ +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; + + public Presence(Element show) { + if ((show == null) || (show.getContent() == null)) { + this.status = Status.ONLINE; + } else if (show.getContent().equals("away")) { + this.status = Status.AWAY; + } else if (show.getContent().equals("xa")) { + this.status = Status.XA; + } else if (show.getContent().equals("chat")) { + this.status = Status.CHAT; + } else if (show.getContent().equals("dnd")) { + this.status = Status.DND; + } else { + this.status = Status.OFFLINE; + } + } + + public int compareTo(Object other) { + return this.status.compareTo(((Presence)other).status); + } + + public Status getStatus() { + return this.status; + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Presences.java b/src/main/java/eu/siacs/conversations/entities/Presences.java index 4729a11b9..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 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 presences = new Hashtable<>(); - - public Hashtable getPresences() { + public Hashtable 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,32 +31,10 @@ public class Presences { } } - public int getMostAvailableStatus() { - int status = OFFLINE; + public Presence getMostAvailablePresence() { synchronized (this.presences) { - Iterator> it = presences.entrySet().iterator(); - while (it.hasNext()) { - Entry entry = it.next(); - if (entry.getValue() < status) - status = entry.getValue(); - } - } - return status; - } - - 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/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/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index d27182c19..46c8a35d4 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -10,6 +10,7 @@ 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.generator.PresenceGenerator; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.xml.Element; @@ -176,7 +177,7 @@ public class PresenceParser extends AbstractParser implements } } int sizeBefore = contact.getPresences().size(); - contact.updatePresence(presence, Presences.parseShow(packet.findChild("show"))); + contact.updatePresence(presence, new Presence(packet.findChild("show"))); 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/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index a295b2cee..9ad8ea1ca 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -72,6 +72,7 @@ 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.Transferable; import eu.siacs.conversations.entities.TransferablePlaceholder; @@ -596,13 +597,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; } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0af3a921d..352d00caa 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -53,6 +53,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; @@ -859,82 +860,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 +947,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 +994,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)); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 00a501f39..638593374 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; @@ -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); } } From 000f59d614d07ac573c1159ea964ebfee6ecbef5 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 17 Jan 2016 16:46:32 -0500 Subject: [PATCH 6/7] Fetch cached caps result on new presence --- .../eu/siacs/conversations/entities/Presence.java | 5 ++++- .../siacs/conversations/parser/PresenceParser.java | 12 ++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Presence.java b/src/main/java/eu/siacs/conversations/entities/Presence.java index adba74d12..d31bb69d5 100644 --- a/src/main/java/eu/siacs/conversations/entities/Presence.java +++ b/src/main/java/eu/siacs/conversations/entities/Presence.java @@ -22,8 +22,11 @@ public class Presence implements Comparable { } protected final Status status; + protected final ServiceDiscoveryResult disco; + + public Presence(Element show, ServiceDiscoveryResult disco) { + this.disco = disco; - public Presence(Element show) { if ((show == null) || (show.getContent() == null)) { this.status = Status.ONLINE; } else if (show.getContent().equals("away")) { diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 46c8a35d4..88631a91d 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -9,8 +9,8 @@ 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; @@ -177,7 +177,15 @@ public class PresenceParser extends AbstractParser implements } } int sizeBefore = contact.getPresences().size(); - contact.updatePresence(presence, new Presence(packet.findChild("show"))); + + ServiceDiscoveryResult disco = null; + Element caps = packet.findChild("c", "http://jabber.org/protocol/caps"); + if (caps != null) { + disco = mXmppConnectionService.databaseBackend. + findDiscoveryResult(caps.getAttribute("hash"), caps.getAttribute("ver")); + } + contact.updatePresence(presence, new Presence(packet.findChild("show"), disco)); + PgpEngine pgp = mXmppConnectionService.getPgpEngine(); Element x = packet.findChild("x", "jabber:x:signed"); if (pgp != null && x != null) { From ae84ff2f0c01be832075d16556db2136c1b5ac55 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Sun, 17 Jan 2016 17:11:00 -0500 Subject: [PATCH 7/7] Do disco for caps hashes we have never seen Then cache it --- .../conversations/parser/PresenceParser.java | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java index 88631a91d..da7a8ce6d 100644 --- a/src/main/java/eu/siacs/conversations/parser/PresenceParser.java +++ b/src/main/java/eu/siacs/conversations/parser/PresenceParser.java @@ -14,9 +14,11 @@ 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 @@ -161,7 +163,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 presence = from.isBareJid() ? "" : from.getResourcepart(); contact.setPresenceName(packet.findChildContent("nick", "http://jabber.org/protocol/nick")); Avatar avatar = Avatar.parsePresence(packet.findChild("x", "vcard-temp:x:update")); if (avatar != null && !contact.isSelf()) { @@ -180,11 +182,30 @@ public class PresenceParser extends AbstractParser implements ServiceDiscoveryResult disco = null; Element caps = packet.findChild("c", "http://jabber.org/protocol/caps"); + if (caps != null) { disco = mXmppConnectionService.databaseBackend. findDiscoveryResult(caps.getAttribute("hash"), caps.getAttribute("ver")); } - contact.updatePresence(presence, new Presence(packet.findChild("show"), disco)); + + if (disco != null || caps == null) { + contact.updatePresence(presence, new Presence(packet.findChild("show"), disco)); + } else { + IqPacket request = new IqPacket(IqPacket.TYPE.GET); + request.setTo(from); + request.query("http://jabber.org/protocol/disco#info"); + + mXmppConnectionService.sendIqPacket(account, request, new OnIqPacketReceived() { + @Override + public void onIqPacketReceived(Account account, IqPacket discoPacket) { + if (discoPacket.getType() == IqPacket.TYPE.RESULT) { + ServiceDiscoveryResult disco = new ServiceDiscoveryResult(discoPacket); + contact.updatePresence(presence, new Presence(packet.findChild("show"), disco)); + mXmppConnectionService.databaseBackend.insertDiscoveryResult(disco); + } + } + }); + } PgpEngine pgp = mXmppConnectionService.getPgpEngine(); Element x = packet.findChild("x", "jabber:x:signed");