fetch MAM messages
This commit is contained in:
parent
bb2d077b7c
commit
58c5bd0f1b
|
@ -2,7 +2,7 @@
|
||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"identityHash": "1780dce1d6aca78c94a2c5c497d158c5",
|
"identityHash": "cc15c6de66482506c7f895ccaff971b4",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "account",
|
"tableName": "account",
|
||||||
|
@ -118,6 +118,86 @@
|
||||||
],
|
],
|
||||||
"foreignKeys": []
|
"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",
|
"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 )",
|
"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": [
|
"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",
|
"name": "index_message_latestVersion",
|
||||||
"unique": false,
|
"unique": false,
|
||||||
|
@ -1735,6 +1806,16 @@
|
||||||
],
|
],
|
||||||
"orders": [],
|
"orders": [],
|
||||||
"createSql": "CREATE INDEX IF NOT EXISTS `index_message_inReplyToMessageEntityId` ON `${TABLE_NAME}` (`inReplyToMessageEntityId`)"
|
"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": [
|
"foreignKeys": [
|
||||||
|
@ -2600,7 +2681,7 @@
|
||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,6 +6,7 @@ import androidx.room.Room;
|
||||||
import androidx.room.RoomDatabase;
|
import androidx.room.RoomDatabase;
|
||||||
import androidx.room.TypeConverters;
|
import androidx.room.TypeConverters;
|
||||||
import im.conversations.android.database.dao.AccountDao;
|
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.AvatarDao;
|
||||||
import im.conversations.android.database.dao.AxolotlDao;
|
import im.conversations.android.database.dao.AxolotlDao;
|
||||||
import im.conversations.android.database.dao.BlockingDao;
|
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.RosterDao;
|
||||||
import im.conversations.android.database.dao.ServiceRecordDao;
|
import im.conversations.android.database.dao.ServiceRecordDao;
|
||||||
import im.conversations.android.database.entity.AccountEntity;
|
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.AvatarAdditionalEntity;
|
||||||
import im.conversations.android.database.entity.AvatarEntity;
|
import im.conversations.android.database.entity.AvatarEntity;
|
||||||
import im.conversations.android.database.entity.AxolotlDeviceListEntity;
|
import im.conversations.android.database.entity.AxolotlDeviceListEntity;
|
||||||
|
@ -56,6 +58,7 @@ import im.conversations.android.database.entity.ServiceRecordCacheEntity;
|
||||||
@Database(
|
@Database(
|
||||||
entities = {
|
entities = {
|
||||||
AccountEntity.class,
|
AccountEntity.class,
|
||||||
|
ArchivePageEntity.class,
|
||||||
AvatarAdditionalEntity.class,
|
AvatarAdditionalEntity.class,
|
||||||
AvatarEntity.class,
|
AvatarEntity.class,
|
||||||
AxolotlDeviceListEntity.class,
|
AxolotlDeviceListEntity.class,
|
||||||
|
@ -114,6 +117,8 @@ public abstract class ConversationsDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
|
||||||
|
public abstract ArchiveDao archiveDao();
|
||||||
|
|
||||||
public abstract AvatarDao avatarDao();
|
public abstract AvatarDao avatarDao();
|
||||||
|
|
||||||
public abstract AxolotlDao axolotlDao();
|
public abstract AxolotlDao axolotlDao();
|
||||||
|
|
|
@ -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<Range> 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<Range> 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);
|
||||||
|
}
|
|
@ -197,24 +197,28 @@ public abstract class ChatDao {
|
||||||
"SELECT c.id,c.accountId,c.address,c.type,m.sentAt,m.outgoing,m.latestVersion as"
|
"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"
|
+ " 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"
|
+ " 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"
|
+ " nick.address=c.address) as nick,(SELECT identity.name FROM disco_item JOIN"
|
||||||
+ " disco_identity identity ON disco_item.discoId=identity.discoId WHERE"
|
+ " disco_identity identity ON disco_item.discoId=identity.discoId WHERE"
|
||||||
+ " disco_item.address=c.address LIMIT 1) as discoIdentityName,(SELECT name FROM"
|
+ " disco_item.accountId=c.accountId AND disco_item.address=c.address LIMIT 1) as"
|
||||||
+ " bookmark WHERE bookmark.address=c.address) as bookmarkName,(CASE WHEN"
|
+ " discoIdentityName,(SELECT name FROM bookmark WHERE"
|
||||||
+ " c.type='MUC' THEN (SELECT vCardPhoto FROM presence WHERE address=c.address AND"
|
+ " bookmark.accountId=c.accountId AND bookmark.address=c.address) as"
|
||||||
+ " resource='') WHEN c.type='INDIVIDUAL' THEN (SELECT vCardPhoto FROM presence"
|
+ " bookmarkName,(CASE WHEN c.type='MUC' THEN (SELECT vCardPhoto FROM presence"
|
||||||
+ " WHERE address=c.address AND vCardPhoto NOT NULL LIMIT 1) ELSE NULL END) as"
|
+ " WHERE presence.accountId=c.accountId AND address=c.address AND resource='')"
|
||||||
+ " vCardPhoto,(SELECT thumb_id FROM avatar WHERE avatar.address=c.address) as"
|
+ " WHEN c.type='INDIVIDUAL' THEN (SELECT vCardPhoto FROM presence WHERE"
|
||||||
+ " avatar FROM CHAT c LEFT JOIN message m ON (c.id=m.chatId) LEFT OUTER JOIN"
|
+ " accountId=c.accountId AND address=c.address AND vCardPhoto NOT NULL LIMIT 1)"
|
||||||
+ " message m2 ON (c.id = m2.chatId AND (m.receivedAt < m2.receivedAt OR"
|
+ " ELSE NULL END) as vCardPhoto,(SELECT thumb_id FROM avatar WHERE"
|
||||||
+ " (m.receivedAt = m2.receivedAt AND m.id < m2.id))) WHERE (:accountId IS NULL OR"
|
+ " 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"
|
+ " c.accountId=:accountId) AND (:groupId IS NULL OR (c.address IN(SELECT"
|
||||||
+ " roster.address FROM roster JOIN roster_group ON"
|
+ " roster.address FROM roster JOIN roster_group ON"
|
||||||
+ " roster.id=roster_group.rosterItemId WHERE roster_group.groupId=:groupId) OR"
|
+ " roster.id=roster_group.rosterItemId WHERE roster.accountId=c.accountId AND"
|
||||||
+ " c.address IN(SELECT address FROM bookmark JOIN bookmark_group ON"
|
+ " roster_group.groupId=:groupId) OR c.address IN(SELECT address FROM bookmark"
|
||||||
+ " bookmark.id=bookmark_group.bookmarkId WHERE bookmark_group.groupId=:groupId)))"
|
+ " JOIN bookmark_group ON bookmark.id=bookmark_group.bookmarkId WHERE"
|
||||||
+ " AND c.archived=0 AND m2.id IS NULL ORDER by m.receivedAt DESC")
|
+ " bookmark.accountId=c.accountId AND bookmark_group.groupId=:groupId))) AND"
|
||||||
|
+ " c.archived=0 ORDER by m.receivedAt DESC")
|
||||||
public abstract PagingSource<Integer, ChatOverviewItem> getChatOverview(
|
public abstract PagingSource<Integer, ChatOverviewItem> getChatOverview(
|
||||||
final Long accountId, final Long groupId);
|
final Long accountId, final Long groupId);
|
||||||
|
|
||||||
|
|
|
@ -454,14 +454,14 @@ public abstract class MessageDao {
|
||||||
+ " inReplyToMessageId=null,inReplyToStanzaId=:stanzaId,inReplyToMessageEntityId=:inReplyToMessageEntityId"
|
+ " inReplyToMessageId=null,inReplyToStanzaId=:stanzaId,inReplyToMessageEntityId=:inReplyToMessageEntityId"
|
||||||
+ " WHERE id=:id")
|
+ " WHERE id=:id")
|
||||||
protected abstract void setInReplyToStanzaId(
|
protected abstract void setInReplyToStanzaId(
|
||||||
final long id, String stanzaId, long inReplyToMessageEntityId);
|
final long id, String stanzaId, Long inReplyToMessageEntityId);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"UPDATE message SET"
|
"UPDATE message SET"
|
||||||
+ " inReplyToMessageId=:messageId,inReplyToStanzaId=null,inReplyToMessageEntityId=:inReplyToMessageEntityId"
|
+ " inReplyToMessageId=:messageId,inReplyToStanzaId=null,inReplyToMessageEntityId=:inReplyToMessageEntityId"
|
||||||
+ " WHERE id=:id")
|
+ " WHERE id=:id")
|
||||||
protected abstract void setInReplyToMessageId(
|
protected abstract void setInReplyToMessageId(
|
||||||
final long id, String messageId, long inReplyToMessageEntityId);
|
final long id, String messageId, Long inReplyToMessageEntityId);
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT id FROM message WHERE chatId=:chatId AND fromBare=:fromBare AND"
|
"SELECT id FROM message WHERE chatId=:chatId AND fromBare=:fromBare AND"
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -32,9 +32,9 @@ import org.jxmpp.jid.parts.Resourcepart;
|
||||||
onDelete = ForeignKey.SET_NULL),
|
onDelete = ForeignKey.SET_NULL),
|
||||||
},
|
},
|
||||||
indices = {
|
indices = {
|
||||||
@Index(value = "chatId"),
|
|
||||||
@Index(value = "latestVersion"),
|
@Index(value = "latestVersion"),
|
||||||
@Index("inReplyToMessageEntityId")
|
@Index("inReplyToMessageEntityId"),
|
||||||
|
@Index(value = {"chatId", "receivedAt"}),
|
||||||
})
|
})
|
||||||
public class MessageEntity {
|
public class MessageEntity {
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package im.conversations.android.database.model;
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
import org.jxmpp.jid.Jid;
|
import org.jxmpp.jid.Jid;
|
||||||
|
|
||||||
|
@ -25,4 +27,13 @@ public class AddressWithName {
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hashCode(address, name);
|
return Objects.hashCode(address, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("address", address)
|
||||||
|
.add("name", name)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package im.conversations.android.database.model;
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
import com.google.common.base.Objects;
|
import com.google.common.base.Objects;
|
||||||
|
|
||||||
public class AvatarWithAccount {
|
public class AvatarWithAccount {
|
||||||
|
@ -10,6 +12,17 @@ public class AvatarWithAccount {
|
||||||
public final AvatarType avatarType;
|
public final AvatarType avatarType;
|
||||||
public final String hash;
|
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(
|
public AvatarWithAccount(
|
||||||
long account,
|
long account,
|
||||||
final AddressWithName addressWithName,
|
final AddressWithName addressWithName,
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package im.conversations.android.database.model;
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
import androidx.room.Relation;
|
import androidx.room.Relation;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import im.conversations.android.database.entity.MessageContentEntity;
|
import im.conversations.android.database.entity.MessageContentEntity;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
@ -131,6 +132,56 @@ public class ChatOverviewItem {
|
||||||
return value != null && !value.trim().isEmpty();
|
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 sealed interface Sender permits SenderYou, SenderName {}
|
||||||
|
|
||||||
public static final class SenderYou implements Sender {}
|
public static final class SenderYou implements Sender {}
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package im.conversations.android.database.model;
|
package im.conversations.android.database.model;
|
||||||
|
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
|
||||||
public class MessageContent {
|
public class MessageContent {
|
||||||
|
|
||||||
public final String language;
|
public final String language;
|
||||||
|
@ -24,4 +26,20 @@ public class MessageContent {
|
||||||
public static MessageContent file(final String url) {
|
public static MessageContent file(final String url) {
|
||||||
return new MessageContent(null, PartType.FILE, null, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package im.conversations.android.transformer;
|
package im.conversations.android.transformer;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import im.conversations.android.database.model.StanzaId;
|
||||||
import im.conversations.android.xml.Namespace;
|
import im.conversations.android.xml.Namespace;
|
||||||
import im.conversations.android.xmpp.Entity;
|
import im.conversations.android.xmpp.Entity;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
|
@ -17,8 +18,8 @@ public class TransformationFactory extends XmppConnection.Delegate {
|
||||||
super(context, connection);
|
super(context, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageTransformation create(final Message message, final String stanzaId) {
|
public MessageTransformation create(final Message message, final StanzaId stanzaId) {
|
||||||
return create(message, stanzaId, Instant.now());
|
return create(message, stanzaId == null ? null : stanzaId.id, Instant.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
public MessageTransformation create(
|
public MessageTransformation create(
|
||||||
|
@ -49,7 +50,7 @@ public class TransformationFactory extends XmppConnection.Delegate {
|
||||||
if (message.getType() == Message.Type.GROUPCHAT) {
|
if (message.getType() == Message.Type.GROUPCHAT) {
|
||||||
senderIdentity = null; // TODO discover real jid
|
senderIdentity = null; // TODO discover real jid
|
||||||
} else {
|
} else {
|
||||||
senderIdentity = from == null ? null : from.asBareJid();
|
senderIdentity = from == null ? boundAddress : from.asBareJid();
|
||||||
}
|
}
|
||||||
return MessageTransformation.of(
|
return MessageTransformation.of(
|
||||||
message, receivedAt, remote, stanzaId, senderIdentity, occupantId);
|
message, receivedAt, remote, stanzaId, senderIdentity, occupantId);
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package im.conversations.android.transformer;
|
package im.conversations.android.transformer;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
import im.conversations.android.axolotl.AxolotlDecryptionException;
|
import im.conversations.android.axolotl.AxolotlDecryptionException;
|
||||||
import im.conversations.android.axolotl.AxolotlService;
|
import im.conversations.android.axolotl.AxolotlService;
|
||||||
import im.conversations.android.database.ConversationsDatabase;
|
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.MessageIdentifier;
|
||||||
import im.conversations.android.database.model.MessageState;
|
import im.conversations.android.database.model.MessageState;
|
||||||
import im.conversations.android.database.model.Modification;
|
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.DeliveryReceipt;
|
||||||
import im.conversations.android.xmpp.model.axolotl.Encrypted;
|
import im.conversations.android.xmpp.model.axolotl.Encrypted;
|
||||||
import im.conversations.android.xmpp.model.correction.Replace;
|
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 im.conversations.android.xmpp.model.stanza.Message;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import org.jxmpp.jid.Jid;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
@ -53,15 +59,45 @@ public class Transformer {
|
||||||
this.axolotlService = axolotlService;
|
this.axolotlService = axolotlService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
public boolean transform(final MessageTransformation transformation) {
|
public boolean transform(final MessageTransformation transformation) {
|
||||||
|
return this.transform(transformation, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean transform(final MessageTransformation transformation, final StanzaId stanzaId) {
|
||||||
return database.runInTransaction(
|
return database.runInTransaction(
|
||||||
() -> {
|
() -> {
|
||||||
final var sendDeliveryReceipts = transform(database, transformation);
|
final var sendDeliveryReceipts = transform(database, transformation);
|
||||||
axolotlService.executePostDecryptionHook();
|
axolotlService.executePostDecryptionHook();
|
||||||
|
if (stanzaId != null) {
|
||||||
|
database.archiveDao().setLivePageStanzaId(account, stanzaId);
|
||||||
|
}
|
||||||
return sendDeliveryReceipts;
|
return sendDeliveryReceipts;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void transform(
|
||||||
|
ImmutableList<MessageTransformation> 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
|
* @param transformation
|
||||||
* @return returns true if there is something we want to send a delivery receipt for. Basically
|
* @return returns true if there is something we want to send a delivery receipt for. Basically
|
||||||
|
|
|
@ -20,6 +20,10 @@ public class ChatOverviewComparator extends DiffUtil.ItemCallback<ChatOverviewIt
|
||||||
@Override
|
@Override
|
||||||
public boolean areContentsTheSame(
|
public boolean areContentsTheSame(
|
||||||
@NonNull ChatOverviewItem oldItem, @NonNull ChatOverviewItem newItem) {
|
@NonNull ChatOverviewItem oldItem, @NonNull ChatOverviewItem newItem) {
|
||||||
return false;
|
final boolean areContentsTheSame = oldItem.equals(newItem);
|
||||||
|
if (!areContentsTheSame) {
|
||||||
|
LOGGER.info("chat {} got modified", oldItem.id);
|
||||||
|
}
|
||||||
|
return areContentsTheSame;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -88,6 +88,7 @@ public final class Namespace {
|
||||||
public static final String REGISTER = "jabber:iq:register";
|
public static final String REGISTER = "jabber:iq:register";
|
||||||
public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register";
|
public static final String REGISTER_STREAM_FEATURE = "http://jabber.org/features/iq-register";
|
||||||
public static final String REPLY = "urn:xmpp:reply:0";
|
public static final String REPLY = "urn:xmpp:reply:0";
|
||||||
|
public static final String RESULT_SET_MANAGEMENT = "http://jabber.org/protocol/rsm";
|
||||||
public static final String RETRACT = "urn:xmpp:message-retract:0";
|
public static final String RETRACT = "urn:xmpp:message-retract:0";
|
||||||
public static final String ROSTER = "jabber:iq:roster";
|
public static final String ROSTER = "jabber:iq:roster";
|
||||||
public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl";
|
public static final String SASL = "urn:ietf:params:xml:ns:xmpp-sasl";
|
||||||
|
|
31
app/src/main/java/im/conversations/android/xmpp/Page.java
Normal file
31
app/src/main/java/im/conversations/android/xmpp/Page.java
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package im.conversations.android.xmpp;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
|
||||||
|
public class Page {
|
||||||
|
|
||||||
|
public final String first;
|
||||||
|
public final String last;
|
||||||
|
public final Integer count;
|
||||||
|
|
||||||
|
public Page(String first, String last, Integer count) {
|
||||||
|
this.first = first;
|
||||||
|
this.last = last;
|
||||||
|
this.count = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Page emptyWithCount(final String id, final Integer count) {
|
||||||
|
return new Page(id, id, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("first", first)
|
||||||
|
.add("last", last)
|
||||||
|
.add("count", count)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
26
app/src/main/java/im/conversations/android/xmpp/Range.java
Normal file
26
app/src/main/java/im/conversations/android/xmpp/Range.java
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package im.conversations.android.xmpp;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
|
||||||
|
public class Range {
|
||||||
|
|
||||||
|
public final Order order;
|
||||||
|
public final String id;
|
||||||
|
|
||||||
|
public Range(final Order order, final String id) {
|
||||||
|
this.order = order;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this).add("order", order).add("id", id).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Order {
|
||||||
|
NORMAL,
|
||||||
|
REVERSE
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +1,50 @@
|
||||||
package im.conversations.android.xmpp.manager;
|
package im.conversations.android.xmpp.manager;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
import com.google.common.base.Strings;
|
||||||
|
import com.google.common.collect.ImmutableList;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
import com.google.common.util.concurrent.FutureCallback;
|
||||||
|
import com.google.common.util.concurrent.Futures;
|
||||||
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
import com.google.common.util.concurrent.MoreExecutors;
|
||||||
|
import im.conversations.android.IDs;
|
||||||
|
import im.conversations.android.database.ConversationsDatabase;
|
||||||
|
import im.conversations.android.transformer.MessageTransformation;
|
||||||
import im.conversations.android.transformer.TransformationFactory;
|
import im.conversations.android.transformer.TransformationFactory;
|
||||||
|
import im.conversations.android.transformer.Transformer;
|
||||||
|
import im.conversations.android.xmpp.Page;
|
||||||
|
import im.conversations.android.xmpp.Range;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
import im.conversations.android.xmpp.model.delay.Delay;
|
import im.conversations.android.xmpp.model.delay.Delay;
|
||||||
|
import im.conversations.android.xmpp.model.mam.Fin;
|
||||||
|
import im.conversations.android.xmpp.model.mam.Query;
|
||||||
import im.conversations.android.xmpp.model.mam.Result;
|
import im.conversations.android.xmpp.model.mam.Result;
|
||||||
|
import im.conversations.android.xmpp.model.rsm.Set;
|
||||||
|
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||||
import im.conversations.android.xmpp.model.stanza.Message;
|
import im.conversations.android.xmpp.model.stanza.Message;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.jxmpp.jid.Jid;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public class ArchiveManager extends AbstractManager {
|
public class ArchiveManager extends AbstractManager {
|
||||||
|
|
||||||
|
private static final int MAX_ITEMS_PER_PAGE = 50;
|
||||||
|
private static final int MAX_PAGES_REVERSING = 20;
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveManager.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveManager.class);
|
||||||
|
|
||||||
private final TransformationFactory transformationFactory;
|
private final TransformationFactory transformationFactory;
|
||||||
|
|
||||||
|
private final Map<QueryId, RunningQuery> runningQueryMap = new HashMap<>();
|
||||||
|
|
||||||
public ArchiveManager(Context context, XmppConnection connection) {
|
public ArchiveManager(Context context, XmppConnection connection) {
|
||||||
super(context, connection);
|
super(context, connection);
|
||||||
this.transformationFactory = new TransformationFactory(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");
|
Preconditions.checkArgument(result != null, "The message needs to contain a MAM result");
|
||||||
final var from = message.getFrom();
|
final var from = message.getFrom();
|
||||||
final var stanzaId = result.getId();
|
final var stanzaId = result.getId();
|
||||||
final var queryId = result.getQueryId();
|
final var id = result.getQueryId();
|
||||||
final var forwarded = result.getForwarded();
|
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);
|
LOGGER.info("Received invalid MAM result from {} ", from);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -39,10 +68,263 @@ public class ArchiveManager extends AbstractManager {
|
||||||
LOGGER.info("MAM result from {} is missing message or receivedAt (delay)", from);
|
LOGGER.info("MAM result from {} is missing message or receivedAt (delay)", from);
|
||||||
return;
|
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<Metadata> 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<Range> queryRanges) {
|
||||||
|
final var future = queryAsFuture(archive, queryRanges);
|
||||||
|
Futures.addCallback(
|
||||||
|
future,
|
||||||
|
new FutureCallback<>() {
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Stats> 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<List<Stats>> queryAsFuture(
|
||||||
|
final Jid archive, final List<Range> queryRanges) {
|
||||||
|
final var queryFutures = Lists.transform(queryRanges, qr -> queryAsFuture(archive, qr));
|
||||||
|
return Futures.allAsList(queryFutures);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<Stats> queryAsFuture(final Jid archive, final Range queryRange) {
|
||||||
|
return queryAsFuture(archive, queryRange, Stats.begin());
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListenableFuture<Stats> 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<Stats> 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<MessageTransformation> 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -243,9 +243,9 @@ public class JingleConnectionManager extends AbstractManager {
|
||||||
|
|
||||||
public void handle(final Message message) {
|
public void handle(final Message message) {
|
||||||
final String id = message.getId();
|
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);
|
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(
|
private void deliverMessage(
|
||||||
|
|
|
@ -14,6 +14,7 @@ import im.conversations.android.database.model.MucWithNick;
|
||||||
import im.conversations.android.xml.Namespace;
|
import im.conversations.android.xml.Namespace;
|
||||||
import im.conversations.android.xmpp.Entity;
|
import im.conversations.android.xmpp.Entity;
|
||||||
import im.conversations.android.xmpp.IqErrorException;
|
import im.conversations.android.xmpp.IqErrorException;
|
||||||
|
import im.conversations.android.xmpp.Range;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
|
import im.conversations.android.xmpp.model.disco.info.InfoQuery;
|
||||||
import im.conversations.android.xmpp.model.error.Condition;
|
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) {
|
private void enterExisting(final MucWithNick mucWithNick, final InfoQuery infoQuery) {
|
||||||
if (infoQuery.hasFeature(Namespace.MUC)
|
if (infoQuery.hasFeature(Namespace.MUC)
|
||||||
&& infoQuery.hasIdentityWithCategory("conference")) {
|
&& infoQuery.hasIdentityWithCategory("conference")) {
|
||||||
|
// TODO check if server has MAM support
|
||||||
|
final var archive = mucWithNick.address;
|
||||||
|
final List<Range> queryRanges =
|
||||||
|
getDatabase().archiveDao().resetLivePage(getAccount(), archive);
|
||||||
sendJoinPresence(mucWithNick);
|
sendJoinPresence(mucWithNick);
|
||||||
|
getManager(ArchiveManager.class).query(archive, queryRanges);
|
||||||
} else {
|
} else {
|
||||||
getDatabase().chatDao().setMucState(mucWithNick.chatId, MucState.NOT_A_MUC);
|
getDatabase().chatDao().setMucState(mucWithNick.chatId, MucState.NOT_A_MUC);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
package im.conversations.android.xmpp.manager;
|
package im.conversations.android.xmpp.manager;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import im.conversations.android.database.model.StanzaId;
|
||||||
import im.conversations.android.xml.Namespace;
|
import im.conversations.android.xml.Namespace;
|
||||||
import im.conversations.android.xmpp.Entity;
|
import im.conversations.android.xmpp.Entity;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
import im.conversations.android.xmpp.XmppConnection;
|
||||||
import im.conversations.android.xmpp.model.stanza.Message;
|
import im.conversations.android.xmpp.model.stanza.Message;
|
||||||
import im.conversations.android.xmpp.model.unique.StanzaId;
|
|
||||||
import org.jxmpp.jid.Jid;
|
import org.jxmpp.jid.Jid;
|
||||||
|
|
||||||
public class StanzaIdManager extends AbstractManager {
|
public class StanzaIdManager extends AbstractManager {
|
||||||
|
@ -14,7 +14,7 @@ public class StanzaIdManager extends AbstractManager {
|
||||||
super(context, connection);
|
super(context, connection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStanzaId(final Message message) {
|
public StanzaId getStanzaId(final Message message) {
|
||||||
final Jid by;
|
final Jid by;
|
||||||
if (message.getType() == Message.Type.GROUPCHAT) {
|
if (message.getType() == Message.Type.GROUPCHAT) {
|
||||||
final var from = message.getFrom();
|
final var from = message.getFrom();
|
||||||
|
@ -25,7 +25,7 @@ public class StanzaIdManager extends AbstractManager {
|
||||||
} else {
|
} else {
|
||||||
by = connection.getBoundAddress().asBareJid();
|
by = connection.getBoundAddress().asBareJid();
|
||||||
}
|
}
|
||||||
if (message.hasExtension(StanzaId.class)
|
if (message.hasExtension(im.conversations.android.xmpp.model.unique.StanzaId.class)
|
||||||
&& getManager(DiscoManager.class)
|
&& getManager(DiscoManager.class)
|
||||||
.hasFeature(Entity.discoItem(by), Namespace.STANZA_IDS)) {
|
.hasFeature(Entity.discoItem(by), Namespace.STANZA_IDS)) {
|
||||||
return getStanzaIdBy(message, by);
|
return getStanzaIdBy(message, by);
|
||||||
|
@ -34,11 +34,12 @@ public class StanzaIdManager extends AbstractManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String getStanzaIdBy(final Message message, final Jid by) {
|
private static StanzaId getStanzaIdBy(final Message message, final Jid by) {
|
||||||
for (final StanzaId stanzaId : message.getExtensions(StanzaId.class)) {
|
for (final var stanzaId :
|
||||||
|
message.getExtensions(im.conversations.android.xmpp.model.unique.StanzaId.class)) {
|
||||||
final var id = stanzaId.getId();
|
final var id = stanzaId.getId();
|
||||||
if (by.equals(stanzaId.getBy()) && id != null) {
|
if (by.equals(stanzaId.getBy()) && id != null) {
|
||||||
return id;
|
return new StanzaId(id, by);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
|
@ -1,21 +1,19 @@
|
||||||
package im.conversations.android.xmpp.processor;
|
package im.conversations.android.xmpp.processor;
|
||||||
|
|
||||||
import android.content.Context;
|
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.xml.Namespace;
|
||||||
import im.conversations.android.xmpp.Entity;
|
import im.conversations.android.xmpp.Entity;
|
||||||
|
import im.conversations.android.xmpp.Range;
|
||||||
import im.conversations.android.xmpp.XmppConnection;
|
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.AxolotlManager;
|
||||||
import im.conversations.android.xmpp.manager.BlockingManager;
|
import im.conversations.android.xmpp.manager.BlockingManager;
|
||||||
import im.conversations.android.xmpp.manager.BookmarkManager;
|
import im.conversations.android.xmpp.manager.BookmarkManager;
|
||||||
import im.conversations.android.xmpp.manager.DiscoManager;
|
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.PresenceManager;
|
||||||
import im.conversations.android.xmpp.manager.RosterManager;
|
import im.conversations.android.xmpp.manager.RosterManager;
|
||||||
|
import java.util.List;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import okhttp3.MediaType;
|
|
||||||
import org.jxmpp.jid.Jid;
|
import org.jxmpp.jid.Jid;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
@ -32,12 +30,14 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer<J
|
||||||
public void accept(final Jid jid) {
|
public void accept(final Jid jid) {
|
||||||
final var account = getAccount();
|
final var account = getAccount();
|
||||||
final var database = getDatabase();
|
final var database = getDatabase();
|
||||||
|
final var archive = jid.asBareJid();
|
||||||
|
final List<Range> catchUpQueryRanges =
|
||||||
database.runInTransaction(
|
database.runInTransaction(
|
||||||
() -> {
|
() -> {
|
||||||
database.chatDao().resetMucStates();
|
database.chatDao().resetMucStates();
|
||||||
database.presenceDao().deletePresences(account.id);
|
database.presenceDao().deletePresences(account.id);
|
||||||
database.discoDao().deleteUnused(account.id);
|
database.discoDao().deleteUnused(account.id);
|
||||||
|
return database.archiveDao().resetLivePage(account, archive);
|
||||||
});
|
});
|
||||||
|
|
||||||
getManager(RosterManager.class).fetch();
|
getManager(RosterManager.class).fetch();
|
||||||
|
@ -57,24 +57,8 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer<J
|
||||||
|
|
||||||
getManager(AxolotlManager.class).publishIfNecessary();
|
getManager(AxolotlManager.class).publishIfNecessary();
|
||||||
|
|
||||||
|
getManager(ArchiveManager.class).query(archive, catchUpQueryRanges);
|
||||||
|
|
||||||
getManager(PresenceManager.class).sendPresence();
|
getManager(PresenceManager.class).sendPresence();
|
||||||
|
|
||||||
final var future =
|
|
||||||
getManager(HttpUploadManager.class)
|
|
||||||
.request("foo.jpg", 123, MediaType.get("image/jpeg"));
|
|
||||||
Futures.addCallback(
|
|
||||||
future,
|
|
||||||
new FutureCallback<HttpUploadManager.Slot>() {
|
|
||||||
@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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -87,7 +87,7 @@ public class MessageProcessor extends XmppConnection.Delegate implements Consume
|
||||||
final var axolotlService =
|
final var axolotlService =
|
||||||
connection.getManager(AxolotlManager.class).getAxolotlService();
|
connection.getManager(AxolotlManager.class).getAxolotlService();
|
||||||
final var transformer = new Transformer(getAccount(), database, axolotlService);
|
final var transformer = new Transformer(getAccount(), database, axolotlService);
|
||||||
sendReceipts = transformer.transform(transformation);
|
sendReceipts = transformer.transform(transformation, stanzaId);
|
||||||
} else {
|
} else {
|
||||||
sendReceipts = true;
|
sendReceipts = true;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue