From 58c5bd0f1b4d026dba6ba792f5aa8b0976b49cc5 Mon Sep 17 00:00:00 2001 From: Daniel Gultsch Date: Fri, 10 Mar 2023 20:03:02 +0100 Subject: [PATCH] fetch MAM messages --- .../1.json | 103 +++++- .../database/ConversationsDatabase.java | 5 + .../android/database/dao/ArchiveDao.java | 174 +++++++++++ .../android/database/dao/ChatDao.java | 32 +- .../android/database/dao/MessageDao.java | 4 +- .../database/entity/ArchivePageEntity.java | 63 ++++ .../database/entity/MessageEntity.java | 4 +- .../database/model/AddressWithName.java | 11 + .../database/model/AvatarWithAccount.java | 13 + .../database/model/ChatOverviewItem.java | 51 +++ .../database/model/MessageContent.java | 18 ++ .../android/database/model/StanzaId.java | 23 ++ .../transformer/TransformationFactory.java | 7 +- .../android/transformer/Transformer.java | 36 +++ .../ui/adapter/ChatOverviewComparator.java | 6 +- .../conversations/android/xml/Namespace.java | 1 + .../im/conversations/android/xmpp/Page.java | 31 ++ .../im/conversations/android/xmpp/Range.java | 26 ++ .../android/xmpp/manager/ArchiveManager.java | 292 +++++++++++++++++- .../xmpp/manager/JingleConnectionManager.java | 4 +- .../xmpp/manager/MultiUserChatManager.java | 6 + .../android/xmpp/manager/StanzaIdManager.java | 13 +- .../android/xmpp/model/mam/End.java | 15 + .../android/xmpp/model/mam/Fin.java | 16 + .../android/xmpp/model/mam/Metadata.java | 20 ++ .../android/xmpp/model/mam/Query.java | 16 + .../android/xmpp/model/mam/Start.java | 16 + .../android/xmpp/model/rsm/After.java | 12 + .../android/xmpp/model/rsm/Before.java | 12 + .../android/xmpp/model/rsm/Count.java | 23 ++ .../android/xmpp/model/rsm/First.java | 12 + .../android/xmpp/model/rsm/Last.java | 12 + .../android/xmpp/model/rsm/Max.java | 16 + .../android/xmpp/model/rsm/Set.java | 55 ++++ .../android/xmpp/model/rsm/package-info.java | 5 + .../android/xmpp/processor/BindProcessor.java | 44 +-- .../xmpp/processor/MessageProcessor.java | 2 +- 37 files changed, 1122 insertions(+), 77 deletions(-) create mode 100644 app/src/main/java/im/conversations/android/database/dao/ArchiveDao.java create mode 100644 app/src/main/java/im/conversations/android/database/entity/ArchivePageEntity.java create mode 100644 app/src/main/java/im/conversations/android/database/model/StanzaId.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/Page.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/Range.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/mam/End.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/mam/Query.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/mam/Start.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/rsm/After.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/rsm/First.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java create mode 100644 app/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java diff --git a/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json b/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json index c4ddeb3ec..65c3acb49 100644 --- a/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json +++ b/app/schemas/im.conversations.android.database.ConversationsDatabase/1.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 1, - "identityHash": "1780dce1d6aca78c94a2c5c497d158c5", + "identityHash": "cc15c6de66482506c7f895ccaff971b4", "entities": [ { "tableName": "account", @@ -118,6 +118,86 @@ ], "foreignKeys": [] }, + { + "tableName": "archive_page", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `accountId` INTEGER NOT NULL, `archive` TEXT NOT NULL, `type` TEXT NOT NULL, `start` TEXT NOT NULL, `end` TEXT NOT NULL, `reachedMaxPages` INTEGER NOT NULL, FOREIGN KEY(`accountId`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archive", + "columnName": "archive", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "start", + "columnName": "start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "end", + "columnName": "end", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reachedMaxPages", + "columnName": "reachedMaxPages", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_archive_page_accountId_archive_type", + "unique": true, + "columnNames": [ + "accountId", + "archive", + "type" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_archive_page_accountId_archive_type` ON `${TABLE_NAME}` (`accountId`, `archive`, `type`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "accountId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, { "tableName": "avatar_additional", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `avatarId` INTEGER NOT NULL, `avatar_external_url` TEXT, `avatar_external_id` TEXT, `avatar_external_type` TEXT, `avatar_external_bytes` INTEGER NOT NULL, `avatar_external_height` INTEGER NOT NULL, `avatar_external_width` INTEGER NOT NULL, FOREIGN KEY(`avatarId`) REFERENCES `avatar`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", @@ -1709,15 +1789,6 @@ ] }, "indices": [ - { - "name": "index_message_chatId", - "unique": false, - "columnNames": [ - "chatId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_message_chatId` ON `${TABLE_NAME}` (`chatId`)" - }, { "name": "index_message_latestVersion", "unique": false, @@ -1735,6 +1806,16 @@ ], "orders": [], "createSql": "CREATE INDEX IF NOT EXISTS `index_message_inReplyToMessageEntityId` ON `${TABLE_NAME}` (`inReplyToMessageEntityId`)" + }, + { + "name": "index_message_chatId_receivedAt", + "unique": false, + "columnNames": [ + "chatId", + "receivedAt" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_chatId_receivedAt` ON `${TABLE_NAME}` (`chatId`, `receivedAt`)" } ], "foreignKeys": [ @@ -2600,7 +2681,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, '1780dce1d6aca78c94a2c5c497d158c5')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cc15c6de66482506c7f895ccaff971b4')" ] } } \ No newline at end of file diff --git a/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java b/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java index 9d8690723..ef9601072 100644 --- a/app/src/main/java/im/conversations/android/database/ConversationsDatabase.java +++ b/app/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.ArchiveDao; import im.conversations.android.database.dao.AvatarDao; import im.conversations.android.database.dao.AxolotlDao; import im.conversations.android.database.dao.BlockingDao; @@ -19,6 +20,7 @@ import im.conversations.android.database.dao.PresenceDao; import im.conversations.android.database.dao.RosterDao; import im.conversations.android.database.dao.ServiceRecordDao; import im.conversations.android.database.entity.AccountEntity; +import im.conversations.android.database.entity.ArchivePageEntity; import im.conversations.android.database.entity.AvatarAdditionalEntity; import im.conversations.android.database.entity.AvatarEntity; import im.conversations.android.database.entity.AxolotlDeviceListEntity; @@ -56,6 +58,7 @@ import im.conversations.android.database.entity.ServiceRecordCacheEntity; @Database( entities = { AccountEntity.class, + ArchivePageEntity.class, AvatarAdditionalEntity.class, AvatarEntity.class, AxolotlDeviceListEntity.class, @@ -114,6 +117,8 @@ public abstract class ConversationsDatabase extends RoomDatabase { public abstract AccountDao accountDao(); + public abstract ArchiveDao archiveDao(); + public abstract AvatarDao avatarDao(); public abstract AxolotlDao axolotlDao(); diff --git a/app/src/main/java/im/conversations/android/database/dao/ArchiveDao.java b/app/src/main/java/im/conversations/android/database/dao/ArchiveDao.java new file mode 100644 index 000000000..6fd792d7e --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/dao/ArchiveDao.java @@ -0,0 +1,174 @@ +package im.conversations.android.database.dao; + +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Query; +import androidx.room.Transaction; +import androidx.room.Upsert; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import im.conversations.android.database.entity.ArchivePageEntity; +import im.conversations.android.database.model.Account; +import im.conversations.android.database.model.StanzaId; +import im.conversations.android.xmpp.Range; +import im.conversations.android.xmpp.manager.ArchiveManager; +import java.util.List; +import java.util.Objects; +import org.jxmpp.jid.Jid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Dao +public abstract class ArchiveDao { + + private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveDao.class); + + @Transaction + public List resetLivePage(final Account account, final Jid archive) { + final var page = + getPage( + account.id, + archive, + ArchivePageEntity.Type.START, + ArchivePageEntity.Type.MIDDLE); + final var livePage = getPage(account.id, archive, ArchivePageEntity.Type.LIVE); + if (page == null && livePage == null) { + LOGGER.info("Emitting initial query for {}", archive); + return ImmutableList.of(new Range(Range.Order.REVERSE, null)); + } + final ImmutableList.Builder queryRangeBuilder = new ImmutableList.Builder<>(); + final boolean gapLess = page != null && livePage != null && page.end.equals(livePage.start); + if (gapLess) { + LOGGER.info("Page and live page for {} were gap-less", archive); + page.end = livePage.end; + insert(page); + if (page.type != ArchivePageEntity.Type.START && !page.reachedMaxPages) { + queryRangeBuilder.add(new Range(Range.Order.REVERSE, page.start)); + } + queryRangeBuilder.add(new Range(Range.Order.NORMAL, livePage.end)); + } else if (page != null) { + LOGGER.info("Ignoring live page for {}", archive); + // this will simply ignore the last live page and overwrite it + if (page.type != ArchivePageEntity.Type.START && !page.reachedMaxPages) { + queryRangeBuilder.add(new Range(Range.Order.REVERSE, page.start)); + } + queryRangeBuilder.add(new Range(Range.Order.NORMAL, page.end)); + } else { + LOGGER.info("Converting live page into regular page for {}", archive); + insert( + ArchivePageEntity.of( + account, + archive, + ArchivePageEntity.Type.MIDDLE, + livePage.start, + livePage.end, + false)); + queryRangeBuilder.add(new Range(Range.Order.REVERSE, livePage.start)); + queryRangeBuilder.add(new Range(Range.Order.NORMAL, livePage.end)); + } + if (livePage != null) { + delete(livePage); + } + return queryRangeBuilder.build(); + } + + public void submitPage( + final Account account, + final Jid archive, + final Range range, + final ArchiveManager.QueryResult queryResult, + final boolean reachedMaxPagesReversing) { + if (reachedMaxPagesReversing) { + Preconditions.checkState( + range.order == Range.Order.REVERSE, + "reachedMaxPagesReversing can only be true when reversing"); + } + final var isComplete = queryResult.isComplete; + final var page = queryResult.page; + + final var existingPage = + getPage( + account.id, + archive, + ArchivePageEntity.Type.START, + ArchivePageEntity.Type.MIDDLE); + final boolean isStart = range.order == Range.Order.REVERSE && isComplete; + if (existingPage == null) { + insert( + ArchivePageEntity.of( + account, + archive, + isStart ? ArchivePageEntity.Type.START : ArchivePageEntity.Type.MIDDLE, + page.first, + page.last, + reachedMaxPagesReversing)); + } else { + if (range.order == Range.Order.REVERSE) { + Preconditions.checkState( + Objects.equals(range.id, existingPage.start), + "Reversing range did not match start of existing page"); + existingPage.start = page.first; + existingPage.type = + isStart ? ArchivePageEntity.Type.START : ArchivePageEntity.Type.MIDDLE; + } else if (range.order == Range.Order.NORMAL) { + Preconditions.checkState( + Objects.equals(range.id, existingPage.end), + "Normal range did not match end of existing page"); + existingPage.end = page.last; + } else { + throw new IllegalStateException(String.format("Unknown order %s", range.order)); + } + existingPage.reachedMaxPages = existingPage.reachedMaxPages || reachedMaxPagesReversing; + insert(existingPage); + } + + final boolean lastIsLive = + (range.order == Range.Order.REVERSE && range.id == null) + || (range.order == Range.Order.NORMAL && queryResult.isComplete); + if (lastIsLive) { + final var existingLivePage = getPage(account.id, archive, ArchivePageEntity.Type.LIVE); + if (existingLivePage != null) { + existingLivePage.start = page.last; + } else { + insert( + ArchivePageEntity.of( + account, + archive, + ArchivePageEntity.Type.LIVE, + page.last, + page.last, + false)); + } + } + } + + public void setLivePageStanzaId(final Account account, final StanzaId stanzaId) { + LOGGER.info("set live page stanza id {}", stanzaId); + final var currentLivePage = getPage(account.id, stanzaId.by, ArchivePageEntity.Type.LIVE); + if (currentLivePage != null) { + currentLivePage.end = stanzaId.id; + insert(currentLivePage); + } else { + insert( + ArchivePageEntity.of( + account, + stanzaId.by, + ArchivePageEntity.Type.LIVE, + stanzaId.id, + stanzaId.id, + false)); + } + } + + @Delete + protected abstract void delete(final ArchivePageEntity entity); + + @Upsert + protected abstract void insert(final ArchivePageEntity entity); + + @Query( + "SELECT * FROM archive_page WHERE accountId=:account AND archive=:archive AND type" + + " IN(:type)") + protected abstract ArchivePageEntity getPage( + long account, Jid archive, ArchivePageEntity.Type... type); +} diff --git a/app/src/main/java/im/conversations/android/database/dao/ChatDao.java b/app/src/main/java/im/conversations/android/database/dao/ChatDao.java index 8a311de85..d67e8e382 100644 --- a/app/src/main/java/im/conversations/android/database/dao/ChatDao.java +++ b/app/src/main/java/im/conversations/android/database/dao/ChatDao.java @@ -197,24 +197,28 @@ public abstract class ChatDao { "SELECT c.id,c.accountId,c.address,c.type,m.sentAt,m.outgoing,m.latestVersion as" + " version,m.toBare,m.toResource,m.fromBare,m.fromResource,(SELECT count(id) FROM" + " message WHERE chatId=c.id) as unread,(SELECT name FROM roster WHERE" - + " roster.address=c.address) as rosterName,(SELECT nick FROM nick WHERE" + + " roster.accountId=c.accountId AND roster.address=c.address) as" + + " rosterName,(SELECT nick FROM nick WHERE nick.accountId=c.accountId AND" + " nick.address=c.address) as nick,(SELECT identity.name FROM disco_item JOIN" + " disco_identity identity ON disco_item.discoId=identity.discoId WHERE" - + " disco_item.address=c.address LIMIT 1) as discoIdentityName,(SELECT name FROM" - + " bookmark WHERE bookmark.address=c.address) as bookmarkName,(CASE WHEN" - + " c.type='MUC' THEN (SELECT vCardPhoto FROM presence WHERE address=c.address AND" - + " resource='') WHEN c.type='INDIVIDUAL' THEN (SELECT vCardPhoto FROM presence" - + " WHERE address=c.address AND vCardPhoto NOT NULL LIMIT 1) ELSE NULL END) as" - + " vCardPhoto,(SELECT thumb_id FROM avatar WHERE avatar.address=c.address) as" - + " avatar FROM CHAT c LEFT JOIN message m ON (c.id=m.chatId) LEFT OUTER JOIN" - + " message m2 ON (c.id = m2.chatId AND (m.receivedAt < m2.receivedAt OR" - + " (m.receivedAt = m2.receivedAt AND m.id < m2.id))) WHERE (:accountId IS NULL OR" + + " disco_item.accountId=c.accountId AND disco_item.address=c.address LIMIT 1) as" + + " discoIdentityName,(SELECT name FROM bookmark WHERE" + + " bookmark.accountId=c.accountId AND bookmark.address=c.address) as" + + " bookmarkName,(CASE WHEN c.type='MUC' THEN (SELECT vCardPhoto FROM presence" + + " WHERE presence.accountId=c.accountId AND address=c.address AND resource='')" + + " WHEN c.type='INDIVIDUAL' THEN (SELECT vCardPhoto FROM presence WHERE" + + " accountId=c.accountId AND address=c.address AND vCardPhoto NOT NULL LIMIT 1)" + + " ELSE NULL END) as vCardPhoto,(SELECT thumb_id FROM avatar WHERE" + + " avatar.accountId=c.accountId AND avatar.address=c.address) as avatar FROM CHAT" + + " c LEFT JOIN message m ON (m.id = (SELECT id FROM message WHERE chatId=c.id" + + " ORDER by receivedAt DESC LIMIT 1)) WHERE (:accountId IS NULL OR" + " c.accountId=:accountId) AND (:groupId IS NULL OR (c.address IN(SELECT" + " roster.address FROM roster JOIN roster_group ON" - + " roster.id=roster_group.rosterItemId WHERE roster_group.groupId=:groupId) OR" - + " c.address IN(SELECT address FROM bookmark JOIN bookmark_group ON" - + " bookmark.id=bookmark_group.bookmarkId WHERE bookmark_group.groupId=:groupId)))" - + " AND c.archived=0 AND m2.id IS NULL ORDER by m.receivedAt DESC") + + " roster.id=roster_group.rosterItemId WHERE roster.accountId=c.accountId AND" + + " roster_group.groupId=:groupId) OR c.address IN(SELECT address FROM bookmark" + + " JOIN bookmark_group ON bookmark.id=bookmark_group.bookmarkId WHERE" + + " bookmark.accountId=c.accountId AND bookmark_group.groupId=:groupId))) AND" + + " c.archived=0 ORDER by m.receivedAt DESC") public abstract PagingSource getChatOverview( final Long accountId, final Long groupId); diff --git a/app/src/main/java/im/conversations/android/database/dao/MessageDao.java b/app/src/main/java/im/conversations/android/database/dao/MessageDao.java index 969aef525..8b7e57c4a 100644 --- a/app/src/main/java/im/conversations/android/database/dao/MessageDao.java +++ b/app/src/main/java/im/conversations/android/database/dao/MessageDao.java @@ -454,14 +454,14 @@ public abstract class MessageDao { + " inReplyToMessageId=null,inReplyToStanzaId=:stanzaId,inReplyToMessageEntityId=:inReplyToMessageEntityId" + " WHERE id=:id") protected abstract void setInReplyToStanzaId( - final long id, String stanzaId, long inReplyToMessageEntityId); + final long id, String stanzaId, Long inReplyToMessageEntityId); @Query( "UPDATE message SET" + " inReplyToMessageId=:messageId,inReplyToStanzaId=null,inReplyToMessageEntityId=:inReplyToMessageEntityId" + " WHERE id=:id") protected abstract void setInReplyToMessageId( - final long id, String messageId, long inReplyToMessageEntityId); + final long id, String messageId, Long inReplyToMessageEntityId); @Query( "SELECT id FROM message WHERE chatId=:chatId AND fromBare=:fromBare AND" diff --git a/app/src/main/java/im/conversations/android/database/entity/ArchivePageEntity.java b/app/src/main/java/im/conversations/android/database/entity/ArchivePageEntity.java new file mode 100644 index 000000000..7f7b5fbbb --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/entity/ArchivePageEntity.java @@ -0,0 +1,63 @@ +package im.conversations.android.database.entity; + +import androidx.annotation.NonNull; +import androidx.room.Entity; +import androidx.room.ForeignKey; +import androidx.room.Index; +import androidx.room.PrimaryKey; +import im.conversations.android.database.model.Account; +import org.jxmpp.jid.Jid; + +@Entity( + tableName = "archive_page", + foreignKeys = + @ForeignKey( + entity = AccountEntity.class, + parentColumns = {"id"}, + childColumns = {"accountId"}, + onDelete = ForeignKey.CASCADE), + indices = { + @Index( + value = {"accountId", "archive", "type"}, + unique = true) + }) +public class ArchivePageEntity { + + @PrimaryKey(autoGenerate = true) + public Long id; + + @NonNull public Long accountId; + + @NonNull public Jid archive; + + @NonNull public Type type; + + @NonNull public String start; + + @NonNull public String end; + + public boolean reachedMaxPages; + + public static ArchivePageEntity of( + final Account account, + final Jid archive, + final Type type, + final String start, + final String end, + final boolean reachedMaxPages) { + final var entity = new ArchivePageEntity(); + entity.accountId = account.id; + entity.archive = archive; + entity.type = type; + entity.start = start; + entity.end = end; + entity.reachedMaxPages = reachedMaxPages; + return entity; + } + + public enum Type { + START, + MIDDLE, + LIVE + } +} diff --git a/app/src/main/java/im/conversations/android/database/entity/MessageEntity.java b/app/src/main/java/im/conversations/android/database/entity/MessageEntity.java index 5ceff7fb2..c929580c2 100644 --- a/app/src/main/java/im/conversations/android/database/entity/MessageEntity.java +++ b/app/src/main/java/im/conversations/android/database/entity/MessageEntity.java @@ -32,9 +32,9 @@ import org.jxmpp.jid.parts.Resourcepart; onDelete = ForeignKey.SET_NULL), }, indices = { - @Index(value = "chatId"), @Index(value = "latestVersion"), - @Index("inReplyToMessageEntityId") + @Index("inReplyToMessageEntityId"), + @Index(value = {"chatId", "receivedAt"}), }) public class MessageEntity { diff --git a/app/src/main/java/im/conversations/android/database/model/AddressWithName.java b/app/src/main/java/im/conversations/android/database/model/AddressWithName.java index e8e0bbba4..ed4675042 100644 --- a/app/src/main/java/im/conversations/android/database/model/AddressWithName.java +++ b/app/src/main/java/im/conversations/android/database/model/AddressWithName.java @@ -1,5 +1,7 @@ package im.conversations.android.database.model; +import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; import com.google.common.base.Objects; import org.jxmpp.jid.Jid; @@ -25,4 +27,13 @@ public class AddressWithName { public int hashCode() { return Objects.hashCode(address, name); } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("address", address) + .add("name", name) + .toString(); + } } diff --git a/app/src/main/java/im/conversations/android/database/model/AvatarWithAccount.java b/app/src/main/java/im/conversations/android/database/model/AvatarWithAccount.java index 8b4d2025d..1236e4943 100644 --- a/app/src/main/java/im/conversations/android/database/model/AvatarWithAccount.java +++ b/app/src/main/java/im/conversations/android/database/model/AvatarWithAccount.java @@ -1,5 +1,7 @@ package im.conversations.android.database.model; +import androidx.annotation.NonNull; +import com.google.common.base.MoreObjects; import com.google.common.base.Objects; public class AvatarWithAccount { @@ -10,6 +12,17 @@ public class AvatarWithAccount { public final AvatarType avatarType; public final String hash; + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("account", account) + .add("addressWithName", addressWithName) + .add("avatarType", avatarType) + .add("hash", hash) + .toString(); + } + public AvatarWithAccount( long account, final AddressWithName addressWithName, diff --git a/app/src/main/java/im/conversations/android/database/model/ChatOverviewItem.java b/app/src/main/java/im/conversations/android/database/model/ChatOverviewItem.java index 02e86dff4..2c9f259d5 100644 --- a/app/src/main/java/im/conversations/android/database/model/ChatOverviewItem.java +++ b/app/src/main/java/im/conversations/android/database/model/ChatOverviewItem.java @@ -1,6 +1,7 @@ package im.conversations.android.database.model; import androidx.room.Relation; +import com.google.common.base.Objects; import com.google.common.collect.Iterables; import im.conversations.android.database.entity.MessageContentEntity; import java.time.Instant; @@ -131,6 +132,56 @@ public class ChatOverviewItem { return value != null && !value.trim().isEmpty(); } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChatOverviewItem that = (ChatOverviewItem) o; + return id == that.id + && accountId == that.accountId + && outgoing == that.outgoing + && version == that.version + && unread == that.unread + && Objects.equal(address, that.address) + && type == that.type + && Objects.equal(sentAt, that.sentAt) + && Objects.equal(toBare, that.toBare) + && Objects.equal(toResource, that.toResource) + && Objects.equal(fromBare, that.fromBare) + && Objects.equal(fromResource, that.fromResource) + && Objects.equal(rosterName, that.rosterName) + && Objects.equal(nick, that.nick) + && Objects.equal(discoIdentityName, that.discoIdentityName) + && Objects.equal(bookmarkName, that.bookmarkName) + && Objects.equal(vCardPhoto, that.vCardPhoto) + && Objects.equal(avatar, that.avatar) + && Objects.equal(contents, that.contents); + } + + @Override + public int hashCode() { + return Objects.hashCode( + id, + accountId, + address, + type, + sentAt, + outgoing, + toBare, + toResource, + fromBare, + fromResource, + version, + rosterName, + nick, + discoIdentityName, + bookmarkName, + vCardPhoto, + avatar, + unread, + contents); + } + public sealed interface Sender permits SenderYou, SenderName {} public static final class SenderYou implements Sender {} diff --git a/app/src/main/java/im/conversations/android/database/model/MessageContent.java b/app/src/main/java/im/conversations/android/database/model/MessageContent.java index 98ceb3ffc..5343ac70b 100644 --- a/app/src/main/java/im/conversations/android/database/model/MessageContent.java +++ b/app/src/main/java/im/conversations/android/database/model/MessageContent.java @@ -1,5 +1,7 @@ package im.conversations.android.database.model; +import com.google.common.base.Objects; + public class MessageContent { public final String language; @@ -24,4 +26,20 @@ public class MessageContent { public static MessageContent file(final String url) { return new MessageContent(null, PartType.FILE, null, url); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MessageContent that = (MessageContent) o; + return Objects.equal(language, that.language) + && type == that.type + && Objects.equal(body, that.body) + && Objects.equal(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hashCode(language, type, body, url); + } } diff --git a/app/src/main/java/im/conversations/android/database/model/StanzaId.java b/app/src/main/java/im/conversations/android/database/model/StanzaId.java new file mode 100644 index 000000000..247ed896f --- /dev/null +++ b/app/src/main/java/im/conversations/android/database/model/StanzaId.java @@ -0,0 +1,23 @@ +package im.conversations.android.database.model; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; +import org.jxmpp.jid.Jid; + +public class StanzaId { + + public final String id; + public final Jid by; + + public StanzaId(String id, Jid by) { + Preconditions.checkNotNull(id); + Preconditions.checkNotNull(by); + this.id = id; + this.by = by; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this).add("id", id).add("by", by).toString(); + } +} diff --git a/app/src/main/java/im/conversations/android/transformer/TransformationFactory.java b/app/src/main/java/im/conversations/android/transformer/TransformationFactory.java index 900fc3ce2..e9ff843a9 100644 --- a/app/src/main/java/im/conversations/android/transformer/TransformationFactory.java +++ b/app/src/main/java/im/conversations/android/transformer/TransformationFactory.java @@ -1,6 +1,7 @@ package im.conversations.android.transformer; import android.content.Context; +import im.conversations.android.database.model.StanzaId; import im.conversations.android.xml.Namespace; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.XmppConnection; @@ -17,8 +18,8 @@ public class TransformationFactory extends XmppConnection.Delegate { super(context, connection); } - public MessageTransformation create(final Message message, final String stanzaId) { - return create(message, stanzaId, Instant.now()); + public MessageTransformation create(final Message message, final StanzaId stanzaId) { + return create(message, stanzaId == null ? null : stanzaId.id, Instant.now()); } public MessageTransformation create( @@ -49,7 +50,7 @@ public class TransformationFactory extends XmppConnection.Delegate { if (message.getType() == Message.Type.GROUPCHAT) { senderIdentity = null; // TODO discover real jid } else { - senderIdentity = from == null ? null : from.asBareJid(); + senderIdentity = from == null ? boundAddress : from.asBareJid(); } return MessageTransformation.of( message, receivedAt, remote, stanzaId, senderIdentity, occupantId); diff --git a/app/src/main/java/im/conversations/android/transformer/Transformer.java b/app/src/main/java/im/conversations/android/transformer/Transformer.java index 36a72aca0..672aca560 100644 --- a/app/src/main/java/im/conversations/android/transformer/Transformer.java +++ b/app/src/main/java/im/conversations/android/transformer/Transformer.java @@ -1,7 +1,9 @@ package im.conversations.android.transformer; import android.content.Context; +import androidx.annotation.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; import im.conversations.android.axolotl.AxolotlDecryptionException; import im.conversations.android.axolotl.AxolotlService; import im.conversations.android.database.ConversationsDatabase; @@ -10,6 +12,9 @@ import im.conversations.android.database.model.ChatIdentifier; import im.conversations.android.database.model.MessageIdentifier; import im.conversations.android.database.model.MessageState; import im.conversations.android.database.model.Modification; +import im.conversations.android.database.model.StanzaId; +import im.conversations.android.xmpp.Range; +import im.conversations.android.xmpp.manager.ArchiveManager; import im.conversations.android.xmpp.model.DeliveryReceipt; import im.conversations.android.xmpp.model.axolotl.Encrypted; import im.conversations.android.xmpp.model.correction.Replace; @@ -21,6 +26,7 @@ import im.conversations.android.xmpp.model.retract.Retract; import im.conversations.android.xmpp.model.stanza.Message; import java.util.Arrays; import java.util.Objects; +import org.jxmpp.jid.Jid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -53,15 +59,45 @@ public class Transformer { this.axolotlService = axolotlService; } + @VisibleForTesting public boolean transform(final MessageTransformation transformation) { + return this.transform(transformation, null); + } + + public boolean transform(final MessageTransformation transformation, final StanzaId stanzaId) { return database.runInTransaction( () -> { final var sendDeliveryReceipts = transform(database, transformation); axolotlService.executePostDecryptionHook(); + if (stanzaId != null) { + database.archiveDao().setLivePageStanzaId(account, stanzaId); + } return sendDeliveryReceipts; }); } + public void transform( + ImmutableList messageTransformations, + final Jid archive, + Range queryRange, + ArchiveManager.QueryResult queryResult, + final boolean reachedMaxPagesReversing) { + database.runInTransaction( + () -> { + for (final MessageTransformation transformation : messageTransformations) { + transform(database, transformation); + } + database.archiveDao() + .submitPage( + account, + archive, + queryRange, + queryResult, + reachedMaxPagesReversing); + axolotlService.executePostDecryptionHook(); + }); + } + /** * @param transformation * @return returns true if there is something we want to send a delivery receipt for. Basically diff --git a/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewComparator.java b/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewComparator.java index 60d4ef221..ba2641dd3 100644 --- a/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewComparator.java +++ b/app/src/main/java/im/conversations/android/ui/adapter/ChatOverviewComparator.java @@ -20,6 +20,10 @@ public class ChatOverviewComparator extends DiffUtil.ItemCallback runningQueryMap = new HashMap<>(); + public ArchiveManager(Context context, XmppConnection connection) { super(context, connection); this.transformationFactory = new TransformationFactory(context, connection); @@ -26,9 +55,9 @@ public class ArchiveManager extends AbstractManager { Preconditions.checkArgument(result != null, "The message needs to contain a MAM result"); final var from = message.getFrom(); final var stanzaId = result.getId(); - final var queryId = result.getQueryId(); + final var id = result.getQueryId(); final var forwarded = result.getForwarded(); - if (forwarded == null || queryId == null || stanzaId == null) { + if (forwarded == null || id == null || stanzaId == null) { LOGGER.info("Received invalid MAM result from {} ", from); return; } @@ -39,10 +68,263 @@ public class ArchiveManager extends AbstractManager { LOGGER.info("MAM result from {} is missing message or receivedAt (delay)", from); return; } - // TODO get query based on queryId and from + final Jid archive = from == null ? connection.getBoundAddress().asBareJid() : from; + final RunningQuery runningQuery; + synchronized (this.runningQueryMap) { + runningQuery = this.runningQueryMap.get(new QueryId(archive, id)); + } + if (runningQuery == null) { + LOGGER.info("Did not find running query for {}/{}", archive, id); + return; + } - final var transformation = this.transformationFactory.create(message, stanzaId, receivedAt); + final var transformation = + this.transformationFactory.create(forwardedMessage, stanzaId, receivedAt); + // TODO only when there is something to transform + runningQuery.addTransformation(transformation); + } - // TODO create transformation; add transformation to Query.Transformer + private ListenableFuture fetchMetadata(final Jid archive) { + final var iq = new Iq(Iq.Type.GET); + iq.setTo(archive); + iq.addExtension(new im.conversations.android.xmpp.model.mam.Metadata()); + final var metadataFuture = connection.sendIqPacket(iq); + return Futures.transform( + metadataFuture, + result -> { + final var metadata = + result.getExtension( + im.conversations.android.xmpp.model.mam.Metadata.class); + if (metadata == null) { + throw new IllegalStateException("result did not contain metadata"); + } + final var start = metadata.getStart(); + final var end = metadata.getEnd(); + if (start == null && end == null) { + return new Metadata(null, null); + } + final var startId = start == null ? null : start.getId(); + final var endId = end == null ? null : end.getId(); + if (Strings.isNullOrEmpty(startId) || Strings.isNullOrEmpty(endId)) { + throw new IllegalStateException("metadata had empty start or end id"); + } + return new Metadata(startId, endId); + }, + MoreExecutors.directExecutor()); + } + + public void query(final Jid archive, final List queryRanges) { + final var future = queryAsFuture(archive, queryRanges); + Futures.addCallback( + future, + new FutureCallback<>() { + @Override + public void onSuccess(List stats) { + LOGGER.info("Successfully queried {} {}", archive, stats); + } + + @Override + public void onFailure(@NonNull Throwable throwable) { + LOGGER.warn("Something went wrong querying {}", archive, throwable); + } + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture> queryAsFuture( + final Jid archive, final List queryRanges) { + final var queryFutures = Lists.transform(queryRanges, qr -> queryAsFuture(archive, qr)); + return Futures.allAsList(queryFutures); + } + + private ListenableFuture queryAsFuture(final Jid archive, final Range queryRange) { + return queryAsFuture(archive, queryRange, Stats.begin()); + } + + private ListenableFuture queryAsFuture( + final Jid archive, final Range queryRange, final Stats stats) { + final var queryId = new QueryId(archive, IDs.medium()); + final var runningQuery = new RunningQuery(queryRange); + final var iq = new Iq(Iq.Type.SET); + iq.setTo(archive); + final var query = iq.addExtension(new Query()); + query.setQueryId(queryId.id); + query.addExtension(Set.of(queryRange, MAX_ITEMS_PER_PAGE)); + synchronized (this.runningQueryMap) { + this.runningQueryMap.put(queryId, runningQuery); + } + final var queryResultFuture = connection.sendIqPacket(iq); + return Futures.transformAsync( + queryResultFuture, + result -> { + final var fin = result.getExtension(Fin.class); + if (fin == null) { + throw new IllegalStateException("Iq response is missing fin element"); + } + final var set = fin.getExtension(Set.class); + if (set == null) { + throw new IllegalStateException("Fin element is missing set element"); + } + final QueryResult queryResult; + if (set.isEmpty()) { + // we fake an empty page here because on catch up queries we the live page + // to be properly reconfigured + queryResult = + new QueryResult( + true, Page.emptyWithCount(queryRange.id, set.getCount())); + } else { + queryResult = new QueryResult(fin.isComplete(), set.asPage()); + } + return processQueryResponse(queryId, queryResult, stats); + }, + MoreExecutors.directExecutor()); + } + + private ListenableFuture processQueryResponse( + final QueryId queryId, final QueryResult queryResult, final Stats existingStats) { + final RunningQuery runningQuery; + synchronized (this.runningQueryMap) { + runningQuery = this.runningQueryMap.remove(queryId); + } + if (runningQuery == null) { + return Futures.immediateFailedFuture( + new IllegalStateException( + String.format( + "Could not find running query for %s/%s", + queryId.archive, queryId.id))); + } + final var messageTransformations = runningQuery.transformationBuilder.build(); + final var stats = existingStats.countPage(messageTransformations.size()); + final boolean reachedMaxPagesReversing = + runningQuery.queryRange.order == Range.Order.REVERSE + && stats.pages >= MAX_PAGES_REVERSING; + final var database = ConversationsDatabase.getInstance(context); + final var axolotlService = connection.getManager(AxolotlManager.class).getAxolotlService(); + final var transformer = new Transformer(getAccount(), database, axolotlService); + + transformer.transform( + messageTransformations, + queryId.archive, + runningQuery.queryRange, + queryResult, + reachedMaxPagesReversing); + + if (queryResult.isComplete || reachedMaxPagesReversing) { + return Futures.immediateFuture(stats); + } else { + final Range range = queryResult.nextPage(runningQuery.queryRange.order); + return queryAsFuture(queryId.archive, range, stats); + } + } + + public static final class Metadata { + public final String start; + public final String end; + + public Metadata(String start, String end) { + this.start = start; + this.end = end; + } + + @Override + @NonNull + public String toString() { + return MoreObjects.toStringHelper(this).add("start", start).add("end", end).toString(); + } + } + + public final class QueryResult { + public final boolean isComplete; + public final Page page; + + public QueryResult(boolean isComplete, Page page) { + this.isComplete = isComplete; + this.page = page; + } + + public Range nextPage(final Range.Order order) { + if (isComplete) { + throw new IllegalStateException("Query was complete. There is no next page"); + } + if (order == Range.Order.NORMAL) { + return new Range(Range.Order.NORMAL, page.last); + } else if (order == Range.Order.REVERSE) { + return new Range(Range.Order.REVERSE, page.first); + } else { + throw new IllegalStateException("Unknown order"); + } + } + + @NonNull + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("isComplete", isComplete) + .add("page", page) + .toString(); + } + } + + public static final class RunningQuery { + public final Range queryRange; + private final ImmutableList.Builder transformationBuilder = + new ImmutableList.Builder<>(); + + public RunningQuery(final Range queryRange) { + this.queryRange = queryRange; + } + + public void addTransformation(final MessageTransformation messageTransformation) { + this.transformationBuilder.add(messageTransformation); + } + } + + public static final class QueryId { + public final Jid archive; + public final String id; + + public QueryId(Jid archive, String id) { + this.archive = archive; + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + QueryId queryId = (QueryId) o; + return Objects.equal(archive, queryId.archive) && Objects.equal(id, queryId.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(archive, id); + } + } + + public static class Stats { + public final int pages; + public final int transformations; + + private Stats(int pages, int transformations) { + this.pages = pages; + this.transformations = transformations; + } + + public static Stats begin() { + return new Stats(0, 0); + } + + public Stats countPage(final int transformations) { + return new Stats(this.pages + 1, this.transformations + transformations); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("pages", pages) + .add("transformations", transformations) + .toString(); + } } } diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/JingleConnectionManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/JingleConnectionManager.java index ad46a19a9..65c48d3cb 100644 --- a/app/src/main/java/im/conversations/android/xmpp/manager/JingleConnectionManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/JingleConnectionManager.java @@ -243,9 +243,9 @@ public class JingleConnectionManager extends AbstractManager { public void handle(final Message message) { final String id = message.getId(); - final String stanzaId = getManager(StanzaIdManager.class).getStanzaId(message); + final var stanzaId = getManager(StanzaIdManager.class).getStanzaId(message); final JingleMessage jingleMessage = message.getExtension(JingleMessage.class); - this.deliverMessage(message.getTo(), message.getFrom(), jingleMessage, id, stanzaId); + this.deliverMessage(message.getTo(), message.getFrom(), jingleMessage, id, stanzaId.id); } private void deliverMessage( diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/MultiUserChatManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/MultiUserChatManager.java index ed5a2fb90..1228296f7 100644 --- a/app/src/main/java/im/conversations/android/xmpp/manager/MultiUserChatManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/MultiUserChatManager.java @@ -14,6 +14,7 @@ import im.conversations.android.database.model.MucWithNick; import im.conversations.android.xml.Namespace; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.IqErrorException; +import im.conversations.android.xmpp.Range; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.model.disco.info.InfoQuery; import im.conversations.android.xmpp.model.error.Condition; @@ -65,7 +66,12 @@ public class MultiUserChatManager extends AbstractManager { private void enterExisting(final MucWithNick mucWithNick, final InfoQuery infoQuery) { if (infoQuery.hasFeature(Namespace.MUC) && infoQuery.hasIdentityWithCategory("conference")) { + // TODO check if server has MAM support + final var archive = mucWithNick.address; + final List queryRanges = + getDatabase().archiveDao().resetLivePage(getAccount(), archive); sendJoinPresence(mucWithNick); + getManager(ArchiveManager.class).query(archive, queryRanges); } else { getDatabase().chatDao().setMucState(mucWithNick.chatId, MucState.NOT_A_MUC); } diff --git a/app/src/main/java/im/conversations/android/xmpp/manager/StanzaIdManager.java b/app/src/main/java/im/conversations/android/xmpp/manager/StanzaIdManager.java index 014629879..6fdd185a8 100644 --- a/app/src/main/java/im/conversations/android/xmpp/manager/StanzaIdManager.java +++ b/app/src/main/java/im/conversations/android/xmpp/manager/StanzaIdManager.java @@ -1,11 +1,11 @@ package im.conversations.android.xmpp.manager; import android.content.Context; +import im.conversations.android.database.model.StanzaId; import im.conversations.android.xml.Namespace; import im.conversations.android.xmpp.Entity; import im.conversations.android.xmpp.XmppConnection; import im.conversations.android.xmpp.model.stanza.Message; -import im.conversations.android.xmpp.model.unique.StanzaId; import org.jxmpp.jid.Jid; public class StanzaIdManager extends AbstractManager { @@ -14,7 +14,7 @@ public class StanzaIdManager extends AbstractManager { super(context, connection); } - public String getStanzaId(final Message message) { + public StanzaId getStanzaId(final Message message) { final Jid by; if (message.getType() == Message.Type.GROUPCHAT) { final var from = message.getFrom(); @@ -25,7 +25,7 @@ public class StanzaIdManager extends AbstractManager { } else { by = connection.getBoundAddress().asBareJid(); } - if (message.hasExtension(StanzaId.class) + if (message.hasExtension(im.conversations.android.xmpp.model.unique.StanzaId.class) && getManager(DiscoManager.class) .hasFeature(Entity.discoItem(by), Namespace.STANZA_IDS)) { return getStanzaIdBy(message, by); @@ -34,11 +34,12 @@ public class StanzaIdManager extends AbstractManager { } } - private static String getStanzaIdBy(final Message message, final Jid by) { - for (final StanzaId stanzaId : message.getExtensions(StanzaId.class)) { + private static StanzaId getStanzaIdBy(final Message message, final Jid by) { + for (final var stanzaId : + message.getExtensions(im.conversations.android.xmpp.model.unique.StanzaId.class)) { final var id = stanzaId.getId(); if (by.equals(stanzaId.getBy()) && id != null) { - return id; + return new StanzaId(id, by); } } return null; diff --git a/app/src/main/java/im/conversations/android/xmpp/model/mam/End.java b/app/src/main/java/im/conversations/android/xmpp/model/mam/End.java new file mode 100644 index 000000000..757ed60c6 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/mam/End.java @@ -0,0 +1,15 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class End extends Extension { + public End() { + super(End.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java b/app/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java new file mode 100644 index 000000000..534072647 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/mam/Fin.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Fin extends Extension { + + public Fin() { + super(Fin.class); + } + + public boolean isComplete() { + return this.getAttributeAsBoolean("complete"); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java b/app/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java new file mode 100644 index 000000000..9f05e08fc --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/mam/Metadata.java @@ -0,0 +1,20 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Metadata extends Extension { + + public Metadata() { + super(Metadata.class); + } + + public Start getStart() { + return this.getExtension(Start.class); + } + + public End getEnd() { + return this.getExtension(End.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/mam/Query.java b/app/src/main/java/im/conversations/android/xmpp/model/mam/Query.java new file mode 100644 index 000000000..d8f701d91 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/mam/Query.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Query extends Extension { + + public Query() { + super(Query.class); + } + + public void setQueryId(final String id) { + this.setAttribute("queryid", id); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/mam/Start.java b/app/src/main/java/im/conversations/android/xmpp/model/mam/Start.java new file mode 100644 index 000000000..9ff84b256 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/mam/Start.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.mam; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Start extends Extension { + + public Start() { + super(Start.class); + } + + public String getId() { + return this.getAttribute("id"); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/rsm/After.java b/app/src/main/java/im/conversations/android/xmpp/model/rsm/After.java new file mode 100644 index 000000000..90179bff0 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/rsm/After.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class After extends Extension { + + public After() { + super(After.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java new file mode 100644 index 000000000..c3c6ac1a8 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Before.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Before extends Extension { + + public Before() { + super(Before.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java new file mode 100644 index 000000000..c54f9d5e0 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Count.java @@ -0,0 +1,23 @@ +package im.conversations.android.xmpp.model.rsm; + +import com.google.common.base.Strings; +import com.google.common.primitives.Ints; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Count extends Extension { + + public Count() { + super(Count.class); + } + + public Integer getCount() { + final var content = getContent(); + if (Strings.isNullOrEmpty(content)) { + return null; + } else { + return Ints.tryParse(content); + } + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/rsm/First.java b/app/src/main/java/im/conversations/android/xmpp/model/rsm/First.java new file mode 100644 index 000000000..b976632e4 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/rsm/First.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class First extends Extension { + + public First() { + super(First.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java new file mode 100644 index 000000000..01d53e073 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Last.java @@ -0,0 +1,12 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Last extends Extension { + + public Last() { + super(Last.class); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java new file mode 100644 index 000000000..06908be8b --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Max.java @@ -0,0 +1,16 @@ +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Max extends Extension { + + public Max() { + super(Max.class); + } + + public void setMax(final int max) { + this.setContent(String.valueOf(max)); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java new file mode 100644 index 000000000..6f428565c --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/rsm/Set.java @@ -0,0 +1,55 @@ +package im.conversations.android.xmpp.model.rsm; + +import com.google.common.base.Strings; +import im.conversations.android.annotation.XmlElement; +import im.conversations.android.xmpp.Page; +import im.conversations.android.xmpp.Range; +import im.conversations.android.xmpp.model.Extension; + +@XmlElement +public class Set extends Extension { + + public Set() { + super(Set.class); + } + + public static Set of(final Range range, final Integer max) { + final var set = new Set(); + if (range.order == Range.Order.NORMAL) { + final var after = set.addExtension(new After()); + after.setContent(range.id); + } else if (range.order == Range.Order.REVERSE) { + final var before = set.addExtension(new Before()); + before.setContent(range.id); + } else { + throw new IllegalArgumentException("Invalid order"); + } + if (max != null) { + set.addExtension(new Max()).setMax(max); + } + return set; + } + + public Page asPage() { + final var first = this.getExtension(First.class); + final var last = this.getExtension(Last.class); + + final var firstId = first == null ? null : first.getContent(); + final var lastId = last == null ? null : last.getContent(); + if (Strings.isNullOrEmpty(firstId) || Strings.isNullOrEmpty(lastId)) { + throw new IllegalStateException("Invalid page. Missing first or last"); + } + return new Page(firstId, lastId, this.getCount()); + } + + public boolean isEmpty() { + final var first = this.getExtension(First.class); + final var last = this.getExtension(Last.class); + return first == null && last == null; + } + + public Integer getCount() { + final var count = this.getExtension(Count.class); + return count == null ? null : count.getCount(); + } +} diff --git a/app/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java b/app/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java new file mode 100644 index 000000000..572b19fe1 --- /dev/null +++ b/app/src/main/java/im/conversations/android/xmpp/model/rsm/package-info.java @@ -0,0 +1,5 @@ +@XmlPackage(namespace = Namespace.RESULT_SET_MANAGEMENT) +package im.conversations.android.xmpp.model.rsm; + +import im.conversations.android.annotation.XmlPackage; +import im.conversations.android.xml.Namespace; diff --git a/app/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java b/app/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java index ed6aeba47..d676fa009 100644 --- a/app/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java +++ b/app/src/main/java/im/conversations/android/xmpp/processor/BindProcessor.java @@ -1,21 +1,19 @@ package im.conversations.android.xmpp.processor; import android.content.Context; -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.MoreExecutors; import im.conversations.android.xml.Namespace; import im.conversations.android.xmpp.Entity; +import im.conversations.android.xmpp.Range; import im.conversations.android.xmpp.XmppConnection; +import im.conversations.android.xmpp.manager.ArchiveManager; import im.conversations.android.xmpp.manager.AxolotlManager; import im.conversations.android.xmpp.manager.BlockingManager; import im.conversations.android.xmpp.manager.BookmarkManager; import im.conversations.android.xmpp.manager.DiscoManager; -import im.conversations.android.xmpp.manager.HttpUploadManager; import im.conversations.android.xmpp.manager.PresenceManager; import im.conversations.android.xmpp.manager.RosterManager; +import java.util.List; import java.util.function.Consumer; -import okhttp3.MediaType; import org.jxmpp.jid.Jid; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -32,13 +30,15 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer { - database.chatDao().resetMucStates(); - database.presenceDao().deletePresences(account.id); - database.discoDao().deleteUnused(account.id); - }); + final var archive = jid.asBareJid(); + final List catchUpQueryRanges = + database.runInTransaction( + () -> { + database.chatDao().resetMucStates(); + database.presenceDao().deletePresences(account.id); + database.discoDao().deleteUnused(account.id); + return database.archiveDao().resetLivePage(account, archive); + }); getManager(RosterManager.class).fetch(); @@ -57,24 +57,8 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer() { - @Override - public void onSuccess(HttpUploadManager.Slot result) { - LOGGER.info("requested slot {}", result); - } - - @Override - public void onFailure(Throwable t) { - LOGGER.info("could not request slot", t); - } - }, - MoreExecutors.directExecutor()); } } diff --git a/app/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java b/app/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java index 3b2e5af10..d57ae7405 100644 --- a/app/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java +++ b/app/src/main/java/im/conversations/android/xmpp/processor/MessageProcessor.java @@ -87,7 +87,7 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume final var axolotlService = connection.getManager(AxolotlManager.class).getAxolotlService(); final var transformer = new Transformer(getAccount(), database, axolotlService); - sendReceipts = transformer.transform(transformation); + sendReceipts = transformer.transform(transformation, stanzaId); } else { sendReceipts = true; }