diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 65cb5083e..4149b5201 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.xml; import com.google.common.base.Optional; +import com.google.common.base.Strings; import com.google.common.collect.Collections2; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; @@ -177,14 +178,14 @@ public class Element { public Jid getAttributeAsJid(String name) { final String jid = this.getAttribute(name); - if (jid != null && !jid.isEmpty()) { - try { - return Jid.ofEscaped(jid); - } catch (final IllegalArgumentException e) { - return InvalidJid.of(jid, this instanceof MessagePacket); - } + if (Strings.isNullOrEmpty(jid)) { + return null; + } + try { + return Jid.ofEscaped(jid); + } catch (final IllegalArgumentException e) { + return InvalidJid.of(jid, this instanceof MessagePacket); } - return null; } public Hashtable getAttributes() { @@ -215,6 +216,7 @@ public class Element { return elementOutput.toString(); } + // TODO should ultimately be removed once everything is an extension public final String getName() { return name; } diff --git a/src/main/java/im/conversations/android/database/ConversationsDatabase.java b/src/main/java/im/conversations/android/database/ConversationsDatabase.java index 0d3656900..d455e588f 100644 --- a/src/main/java/im/conversations/android/database/ConversationsDatabase.java +++ b/src/main/java/im/conversations/android/database/ConversationsDatabase.java @@ -8,6 +8,7 @@ import androidx.room.TypeConverters; import im.conversations.android.database.dao.AccountDao; import im.conversations.android.database.dao.MessageDao; import im.conversations.android.database.dao.PresenceDao; +import im.conversations.android.database.dao.RosterDao; import im.conversations.android.database.entity.AccountEntity; import im.conversations.android.database.entity.BlockedItemEntity; import im.conversations.android.database.entity.ChatEntity; @@ -71,4 +72,6 @@ public abstract class ConversationsDatabase extends RoomDatabase { public abstract PresenceDao presenceDao(); public abstract MessageDao messageDao(); + + public abstract RosterDao rosterDao(); } diff --git a/src/main/java/im/conversations/android/database/dao/RosterDao.java b/src/main/java/im/conversations/android/database/dao/RosterDao.java new file mode 100644 index 000000000..68fa3748b --- /dev/null +++ b/src/main/java/im/conversations/android/database/dao/RosterDao.java @@ -0,0 +1,32 @@ +package im.conversations.android.database.dao; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Transaction; +import im.conversations.android.database.entity.RosterItemEntity; +import im.conversations.android.database.model.Account; +import java.util.Collection; + +@Dao +public abstract class RosterDao { + + @Insert + protected abstract void insert(Collection rosterItems); + + @Query("DELETE FROM roster WHERE accountId=:account") + protected abstract void clear(final long account); + + @Query("UPDATE account SET rosterVersion=:version WHERE id=:account") + protected abstract void setRosterVersion(final long account, final String version); + + @Transaction + public void setRoster( + final Account account, + final String version, + final Collection rosterItems) { + clear(account.id); + insert(rosterItems); + setRosterVersion(account.id, version); + } +} diff --git a/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java b/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java index 399ad6933..f520b14b0 100644 --- a/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java +++ b/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java @@ -5,7 +5,10 @@ import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import androidx.room.PrimaryKey; -import im.conversations.android.database.model.Subscription; +import com.google.common.collect.Collections2; +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.roster.Item; +import java.util.Collection; @Entity( tableName = "roster", @@ -27,11 +30,26 @@ public class RosterItemEntity { @NonNull public Long accountId; - @NonNull public String address; + @NonNull public Jid address; - public Subscription subscription; + public Item.Subscription subscription; - public boolean ask; + public boolean isPendingOut; public String name; + + public static RosterItemEntity of(final long accountId, final Item item) { + final var entity = new RosterItemEntity(); + entity.accountId = accountId; + entity.address = item.getJid(); + entity.subscription = item.getSubscription(); + entity.isPendingOut = item.isPendingOut(); + entity.name = item.getItemName(); + return entity; + } + + public static Collection of( + final long accountId, final Collection items) { + return Collections2.transform(items, i -> of(accountId, i)); + } } diff --git a/src/main/java/im/conversations/android/database/model/Subscription.java b/src/main/java/im/conversations/android/database/model/Subscription.java deleted file mode 100644 index 3002b0377..000000000 --- a/src/main/java/im/conversations/android/database/model/Subscription.java +++ /dev/null @@ -1,8 +0,0 @@ -package im.conversations.android.database.model; - -public enum Subscription { - NONE, - TO, - FROM, - BOTH -} diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java index 6aebbcf0f..416ed3143 100644 --- a/src/main/java/im/conversations/android/xmpp/model/roster/Item.java +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Item.java @@ -1,13 +1,49 @@ package im.conversations.android.xmpp.model.roster; import eu.siacs.conversations.xml.Namespace; +import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.annotation.XmlElement; import im.conversations.android.xmpp.model.Extension; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; @XmlElement public class Item extends Extension { + public static final List RESULT_SUBSCRIPTIONS = + Arrays.asList(Subscription.NONE, Subscription.TO, Subscription.FROM, Subscription.BOTH); + public Item() { super("item", Namespace.ROSTER); } + + public Jid getJid() { + return getAttributeAsJid("jid"); + } + + public String getItemName() { + return this.getAttribute("name"); + } + + public boolean isPendingOut() { + return "subscribe".equalsIgnoreCase(this.getAttribute("ask")); + } + + public Subscription getSubscription() { + final String value = this.getAttribute("subscription"); + try { + return value == null ? null : Subscription.valueOf(value.toLowerCase(Locale.ROOT)); + } catch (final IllegalArgumentException e) { + return null; + } + } + + public enum Subscription { + NONE, + TO, + FROM, + BOTH, + REMOVE + } } diff --git a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java index 89e5c9c45..44886d6c7 100644 --- a/src/main/java/im/conversations/android/xmpp/model/roster/Query.java +++ b/src/main/java/im/conversations/android/xmpp/model/roster/Query.java @@ -14,4 +14,8 @@ public class Query extends Extension { public void setVersion(final String rosterVersion) { this.setAttribute("ver", rosterVersion); } + + public String getVersion() { + return this.getAttribute("ver"); + } } diff --git a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index 8b539a430..c0c5f7500 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -3,9 +3,11 @@ package im.conversations.android.xmpp.processor; import android.content.Context; import android.util.Log; import com.google.common.base.Strings; +import com.google.common.collect.Collections2; import eu.siacs.conversations.Config; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.stanzas.IqPacket; +import im.conversations.android.database.entity.RosterItemEntity; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.model.roster.Item; import im.conversations.android.xmpp.model.roster.Query; @@ -52,19 +54,29 @@ public class BindProcessor extends AbstractBaseProcessor implements Consumer { - if (result.getType() != IqPacket.TYPE.RESULT) { - return; - } - final Query query = result.getExtension(Query.class); - if (query == null) { - // No query in result means further modifications are sent via pushes - return; - } - // TODO delete entire roster - for (final Item item : query.getExtensions(Item.class)) {} - }); + connection.sendIqPacket(iqPacket, this::handleFetchRosterResult); + } + + private void handleFetchRosterResult(final IqPacket result) { + if (result.getType() != IqPacket.TYPE.RESULT) { + return; + } + final Query query = result.getExtension(Query.class); + if (query == null) { + // No query in result means further modifications are sent via pushes + return; + } + final var account = getAccount(); + final var database = getDatabase(); + final var version = query.getVersion(); + final var items = query.getExtensions(Item.class); + // In a roster result (Section 2.1.4), the client MUST ignore values of the c'subscription' + // attribute other than "none", "to", "from", or "both". + final var validItems = + Collections2.filter( + items, + i -> i != null && Item.RESULT_SUBSCRIPTIONS.contains(i.getSubscription())); + final var entities = RosterItemEntity.of(account.id, validItems); + database.rosterDao().setRoster(account, version, entities); } }