diff --git a/schemas/im.conversations.android.database.ConversationsDatabase/1.json b/schemas/im.conversations.android.database.ConversationsDatabase/1.json index e0bb78f30..7c25642ed 100644 --- a/schemas/im.conversations.android.database.ConversationsDatabase/1.json +++ b/schemas/im.conversations.android.database.ConversationsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "d16845c3eb73e5fdbc9902903b74428a", + "identityHash": "aa5e73a1cf9ba959e118f66b89b9d227", "entities": [ { "tableName": "account", @@ -1066,7 +1066,7 @@ }, { "tableName": "roster", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `subscription` TEXT, `ask` INTEGER NOT NULL, `name` TEXT, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `address` TEXT NOT NULL, `subscription` TEXT, `isPendingOut` INTEGER NOT NULL, `name` TEXT, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", "fields": [ { "fieldPath": "id", @@ -1093,8 +1093,8 @@ "notNull": false }, { - "fieldPath": "ask", - "columnName": "ask", + "fieldPath": "isPendingOut", + "columnName": "isPendingOut", "affinity": "INTEGER", "notNull": true }, @@ -1195,7 +1195,7 @@ "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd16845c3eb73e5fdbc9902903b74428a')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'aa5e73a1cf9ba959e118f66b89b9d227')" ] } } \ No newline at end of file diff --git a/src/main/java/eu/siacs/conversations/xml/Element.java b/src/main/java/eu/siacs/conversations/xml/Element.java index 4149b5201..964bf2558 100644 --- a/src/main/java/eu/siacs/conversations/xml/Element.java +++ b/src/main/java/eu/siacs/conversations/xml/Element.java @@ -67,6 +67,10 @@ public class Element { return null; } + public boolean hasExtension(final Class clazz) { + return Iterables.any(this.children, clazz::isInstance); + } + public E getExtension(final Class clazz) { final var extension = Iterables.find(this.children, clazz::isInstance); if (extension == null) { diff --git a/src/main/java/im/conversations/android/database/ConversationsDatabase.java b/src/main/java/im/conversations/android/database/ConversationsDatabase.java index d455e588f..58eb4e3c0 100644 --- a/src/main/java/im/conversations/android/database/ConversationsDatabase.java +++ b/src/main/java/im/conversations/android/database/ConversationsDatabase.java @@ -6,6 +6,7 @@ import androidx.room.Room; import androidx.room.RoomDatabase; import androidx.room.TypeConverters; import im.conversations.android.database.dao.AccountDao; +import im.conversations.android.database.dao.BlockingDao; import im.conversations.android.database.dao.MessageDao; import im.conversations.android.database.dao.PresenceDao; import im.conversations.android.database.dao.RosterDao; @@ -74,4 +75,6 @@ public abstract class ConversationsDatabase extends RoomDatabase { public abstract MessageDao messageDao(); public abstract RosterDao rosterDao(); + + public abstract BlockingDao blockingDao(); } diff --git a/src/main/java/im/conversations/android/database/dao/BlockingDao.java b/src/main/java/im/conversations/android/database/dao/BlockingDao.java new file mode 100644 index 000000000..f4c1b1c48 --- /dev/null +++ b/src/main/java/im/conversations/android/database/dao/BlockingDao.java @@ -0,0 +1,29 @@ +package im.conversations.android.database.dao; + +import androidx.room.Dao; +import androidx.room.Insert; +import androidx.room.Query; +import androidx.room.Transaction; +import com.google.common.collect.Collections2; +import im.conversations.android.database.entity.BlockedItemEntity; +import im.conversations.android.database.model.Account; +import im.conversations.android.xmpp.model.blocking.Item; +import java.util.Collection; + +@Dao +public abstract class BlockingDao { + + @Insert + abstract void insert(Collection entities); + + @Query("DELETE FROM blocked WHERE accountId=:account") + abstract void clear(final long account); + + @Transaction + public void setBlocklist(final Account account, final Collection blockedItems) { + final var entities = + Collections2.transform(blockedItems, i -> BlockedItemEntity.of(account.id, i)); + clear(account.id); + insert(entities); + } +} diff --git a/src/main/java/im/conversations/android/database/dao/RosterDao.java b/src/main/java/im/conversations/android/database/dao/RosterDao.java index 68fa3748b..b04f36cf3 100644 --- a/src/main/java/im/conversations/android/database/dao/RosterDao.java +++ b/src/main/java/im/conversations/android/database/dao/RosterDao.java @@ -1,32 +1,56 @@ package im.conversations.android.database.dao; +import static androidx.room.OnConflictStrategy.REPLACE; + import androidx.room.Dao; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Transaction; +import eu.siacs.conversations.xmpp.Jid; import im.conversations.android.database.entity.RosterItemEntity; import im.conversations.android.database.model.Account; +import im.conversations.android.xmpp.model.roster.Item; import java.util.Collection; @Dao public abstract class RosterDao { - @Insert - protected abstract void insert(Collection rosterItems); + @Insert(onConflict = REPLACE) + protected abstract long insert(RosterItemEntity rosterItem); @Query("DELETE FROM roster WHERE accountId=:account") protected abstract void clear(final long account); + @Query("DELETE FROM roster WHERE accountId=:account AND address=:address") + protected abstract void delete(final long account, final Jid address); + @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) { + public void set( + final Account account, final String version, final Collection rosterItems) { clear(account.id); - insert(rosterItems); + for (final Item item : rosterItems) { + final long id = insert(RosterItemEntity.of(account.id, item)); + // TODO insert groups + } + setRosterVersion(account.id, version); + } + + public void update( + final Account account, final String version, final Collection updates) { + for (final Item item : updates) { + final Item.Subscription subscription = item.getSubscription(); + if (subscription == null) { + continue; + } + if (subscription == Item.Subscription.REMOVE) { + delete(account.id, item.getJid()); + } + final RosterItemEntity entity = RosterItemEntity.of(account.id, item); + final long id = insert(entity); + } setRosterVersion(account.id, version); } } diff --git a/src/main/java/im/conversations/android/database/entity/BlockedItemEntity.java b/src/main/java/im/conversations/android/database/entity/BlockedItemEntity.java index f2ba56922..6cd1d8124 100644 --- a/src/main/java/im/conversations/android/database/entity/BlockedItemEntity.java +++ b/src/main/java/im/conversations/android/database/entity/BlockedItemEntity.java @@ -5,6 +5,8 @@ import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import androidx.room.PrimaryKey; +import eu.siacs.conversations.xmpp.Jid; +import im.conversations.android.xmpp.model.blocking.Item; @Entity( tableName = "blocked", @@ -26,5 +28,12 @@ public class BlockedItemEntity { @NonNull public Long accountId; - @NonNull public String address; + @NonNull public Jid address; + + public static BlockedItemEntity of(final long accountId, final Item item) { + final var entity = new BlockedItemEntity(); + entity.accountId = accountId; + entity.address = item.getJid(); + return entity; + } } 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 f520b14b0..a5c9dacd6 100644 --- a/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java +++ b/src/main/java/im/conversations/android/database/entity/RosterItemEntity.java @@ -5,10 +5,8 @@ import androidx.room.Entity; import androidx.room.ForeignKey; import androidx.room.Index; import androidx.room.PrimaryKey; -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", @@ -47,9 +45,4 @@ public class RosterItemEntity { 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/xmpp/Extensions.java b/src/main/java/im/conversations/android/xmpp/Extensions.java index 962973bd1..80244bc84 100644 --- a/src/main/java/im/conversations/android/xmpp/Extensions.java +++ b/src/main/java/im/conversations/android/xmpp/Extensions.java @@ -7,8 +7,6 @@ import eu.siacs.conversations.xml.Element; import im.conversations.android.annotation.XmlElement; import im.conversations.android.annotation.XmlPackage; import im.conversations.android.xmpp.model.Extension; -import im.conversations.android.xmpp.model.roster.Item; -import im.conversations.android.xmpp.model.roster.Query; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; @@ -18,8 +16,15 @@ import java.util.Map; public final class Extensions { + // TODO these two maps can easily be generated by an annotation processor private static final List> ELEMENTS = - Arrays.asList(Query.class, Item.class); + Arrays.asList( + im.conversations.android.xmpp.model.roster.Query.class, + im.conversations.android.xmpp.model.roster.Item.class, + im.conversations.android.xmpp.model.blocking.Item.class, + im.conversations.android.xmpp.model.blocking.Block.class, + im.conversations.android.xmpp.model.blocking.Blocklist.class, + im.conversations.android.xmpp.model.blocking.Unblock.class); private static final Map> EXTENSION_CLASS_MAP; diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java new file mode 100644 index 000000000..9843a8f95 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Block.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.blocking; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Block extends Extension { + + public Block() { + super("block", Namespace.BLOCKING); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java new file mode 100644 index 000000000..dd8591a2c --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Blocklist.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.blocking; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Blocklist extends Extension { + public Blocklist() { + super("blocklist", Namespace.BLOCKING); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java new file mode 100644 index 000000000..97e2f21d5 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Item.java @@ -0,0 +1,18 @@ +package im.conversations.android.xmpp.model.blocking; + +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; + +@XmlElement +public class Item extends Extension { + + public Item() { + super("item", Namespace.BLOCKING); + } + + public Jid getJid() { + return getAttributeAsJid("jid"); + } +} diff --git a/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java new file mode 100644 index 000000000..ee5b22bc3 --- /dev/null +++ b/src/main/java/im/conversations/android/xmpp/model/blocking/Unblock.java @@ -0,0 +1,13 @@ +package im.conversations.android.xmpp.model.blocking; + +import eu.siacs.conversations.xml.Namespace; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Unblock extends Extension { + + public Unblock() { + super("unblock", Namespace.BLOCKING); + } +} 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 c0c5f7500..1e3d05b16 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -7,10 +7,11 @@ 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.blocking.Blocklist; import im.conversations.android.xmpp.model.roster.Item; import im.conversations.android.xmpp.model.roster.Query; +import java.util.Objects; import java.util.function.Consumer; public class BindProcessor extends AbstractBaseProcessor implements Consumer { @@ -36,6 +37,9 @@ public class BindProcessor extends AbstractBaseProcessor implements Consumer i != null && Item.RESULT_SUBSCRIPTIONS.contains(i.getSubscription())); - final var entities = RosterItemEntity.of(account.id, validItems); - database.rosterDao().setRoster(account, version, entities); + i -> + Item.RESULT_SUBSCRIPTIONS.contains(i.getSubscription()) + && Objects.nonNull(i.getJid())); + database.rosterDao().set(account, version, validItems); + } + + private void fetchBlocklist() { + final IqPacket iqPacket = new IqPacket(IqPacket.TYPE.GET); + iqPacket.addChild(new Blocklist()); + connection.sendIqPacket(iqPacket, this::handleFetchBlocklistResult); + } + + private void handleFetchBlocklistResult(final IqPacket result) { + if (result.getType() != IqPacket.TYPE.RESULT) { + return; + } + final var blocklist = result.getExtension(Blocklist.class); + if (blocklist == null) { + return; + } + final var account = getAccount(); + final var items = + blocklist.getExtensions(im.conversations.android.xmpp.model.blocking.Item.class); + final var filteredItems = Collections2.filter(items, i -> Objects.nonNull(i.getJid())); + getDatabase().blockingDao().setBlocklist(account, filteredItems); } } diff --git a/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java b/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java index 1a1002062..d782d2079 100644 --- a/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java +++ b/src/main/java/im/conversations/android/xmpp/processor/IqProcessor.java @@ -1,14 +1,32 @@ package im.conversations.android.xmpp.processor; import android.content.Context; +import com.google.common.base.Preconditions; import eu.siacs.conversations.xmpp.stanzas.IqPacket; import im.conversations.android.xmpp.XmppConnection; +import im.conversations.android.xmpp.model.roster.Query; +import java.util.Arrays; import java.util.function.Consumer; -public class IqProcessor implements Consumer { +public class IqProcessor extends AbstractBaseProcessor implements Consumer { - public IqProcessor(final Context context, final XmppConnection connection) {} + public IqProcessor(final Context context, final XmppConnection connection) { + super(context, connection); + } @Override - public void accept(final IqPacket packet) {} + public void accept(final IqPacket packet) { + final IqPacket.TYPE type = packet.getType(); + Preconditions.checkArgument( + Arrays.asList(IqPacket.TYPE.GET, IqPacket.TYPE.SET).contains(type)); + if (type == IqPacket.TYPE.SET + && connection.fromAccount(packet) + && packet.hasExtension(Query.class)) { + handleRosterPush(packet.getExtension(Query.class)); + } + } + + private void handleRosterPush(final Query query) { + final String version = query.getVersion(); + } }