fetch MAM messages
This commit is contained in:
parent
bb2d077b7c
commit
58c5bd0f1b
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
+ " 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<Integer, ChatOverviewItem> getChatOverview(
|
||||
final Long accountId, final Long groupId);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
indices = {
|
||||
@Index(value = "chatId"),
|
||||
@Index(value = "latestVersion"),
|
||||
@Index("inReplyToMessageEntityId")
|
||||
@Index("inReplyToMessageEntityId"),
|
||||
@Index(value = {"chatId", "receivedAt"}),
|
||||
})
|
||||
public class MessageEntity {
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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);
|
||||
|
|
|
@ -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<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
|
||||
* @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
|
||||
public boolean areContentsTheSame(
|
||||
@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_STREAM_FEATURE = "http://jabber.org/features/iq-register";
|
||||
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 ROSTER = "jabber:iq:roster";
|
||||
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;
|
||||
|
||||
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.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.Transformer;
|
||||
import im.conversations.android.xmpp.Page;
|
||||
import im.conversations.android.xmpp.Range;
|
||||
import im.conversations.android.xmpp.XmppConnection;
|
||||
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.rsm.Set;
|
||||
import im.conversations.android.xmpp.model.stanza.Iq;
|
||||
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.LoggerFactory;
|
||||
|
||||
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 final TransformationFactory transformationFactory;
|
||||
|
||||
private final Map<QueryId, RunningQuery> 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<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) {
|
||||
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(
|
||||
|
|
|
@ -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<Range> queryRanges =
|
||||
getDatabase().archiveDao().resetLivePage(getAccount(), archive);
|
||||
sendJoinPresence(mucWithNick);
|
||||
getManager(ArchiveManager.class).query(archive, queryRanges);
|
||||
} else {
|
||||
getDatabase().chatDao().setMucState(mucWithNick.chatId, MucState.NOT_A_MUC);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
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,12 +30,14 @@ public class BindProcessor extends XmppConnection.Delegate implements Consumer<J
|
|||
public void accept(final Jid jid) {
|
||||
final var account = getAccount();
|
||||
final var database = getDatabase();
|
||||
|
||||
final var archive = jid.asBareJid();
|
||||
final List<Range> 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<J
|
|||
|
||||
getManager(AxolotlManager.class).publishIfNecessary();
|
||||
|
||||
getManager(ArchiveManager.class).query(archive, catchUpQueryRanges);
|
||||
|
||||
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 =
|
||||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue