package com.xabber.android.data.extension.mam; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.xabber.android.data.Application; import com.xabber.android.data.account.AccountItem; import com.xabber.android.data.account.AccountManager; import com.xabber.android.data.connection.ConnectionItem; import com.xabber.android.data.database.MessageDatabaseManager; import com.xabber.android.data.database.messagerealm.MessageItem; import com.xabber.android.data.database.messagerealm.SyncInfo; import com.xabber.android.data.entity.AccountJid; import com.xabber.android.data.entity.BaseEntity; import com.xabber.android.data.entity.UserJid; import com.xabber.android.data.extension.file.FileManager; import com.xabber.android.data.log.LogManager; import com.xabber.android.data.message.AbstractChat; import com.xabber.android.data.message.MessageManager; import com.xabber.android.data.roster.OnRosterReceivedListener; import com.xabber.android.data.roster.RosterContact; import com.xabber.android.data.roster.RosterManager; import net.java.otr4j.io.SerializationUtils; import net.java.otr4j.io.messages.PlainTextMessage; import org.greenrobot.eventbus.EventBus; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.XMPPException; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.tcp.XMPPTCPConnection; import org.jivesoftware.smackx.delay.packet.DelayInformation; import org.jivesoftware.smackx.forward.packet.Forwarded; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import io.realm.Realm; import io.realm.RealmResults; public class MamManager implements OnRosterReceivedListener { static final String LOG_TAG = MamManager.class.getSimpleName(); private static MamManager instance; public static final int SYNC_INTERVAL_MINUTES = 5; public static int PAGE_SIZE = AbstractChat.PRELOADED_MESSAGES; private Map<AccountJid, Boolean> supportedByAccount; public static MamManager getInstance() { if (instance == null) { instance = new MamManager(); } return instance; } public MamManager() { supportedByAccount = new ConcurrentHashMap<>(); } public void onAuthorized(ConnectionItem connectionItem) { updateIsSupported((AccountItem) connectionItem); } @Override public void onRosterReceived(final AccountItem accountItem) { LogManager.i(this, "onRosterReceived " + accountItem.getAccount()); Application.getInstance().runOnUiThread(new Runnable() { @Override public void run() { if (accountItem.getLoadHistorySettings() != LoadHistorySettings.all) { return; } Collection<RosterContact> contacts = RosterManager.getInstance() .getAccountRosterContacts(accountItem.getAccount()); for (RosterContact contact : contacts) { requestLastHistory(MessageManager.getInstance() .getOrCreateChat(contact.getAccount(), contact.getUser())); } } }); } @Nullable public Boolean isSupported(AccountJid accountJid) { return supportedByAccount.get(accountJid); } private boolean checkSupport(AccountItem accountItem) { Boolean isSupported = supportedByAccount.get(accountItem.getAccount()); if (isSupported != null) { return isSupported; } return updateIsSupported(accountItem); } private boolean updateIsSupported(AccountItem accountItem) { org.jivesoftware.smackx.mam.MamManager mamManager = org.jivesoftware.smackx.mam.MamManager .getInstanceFor(accountItem.getConnection()); boolean isSupported; try { isSupported = mamManager.isSupportedByServer(); if (isSupported) { org.jivesoftware.smackx.mam.MamManager.MamPrefsResult archivingPreferences = mamManager.retrieveArchivingPreferences(); LogManager.i(this, "archivingPreferences default behaviour " + archivingPreferences.mamPrefs.getDefault()); org.jivesoftware.smackx.mam.MamManager.MamPrefsResult result = mamManager.updateArchivingPreferences(null, null, accountItem.getMamDefaultBehaviour()); LogManager.i(this, "updateArchivingPreferences result " + result.toString()); } } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException | InterruptedException | SmackException.NotConnectedException | SmackException.NotLoggedInException e) { LogManager.exception(this, e); return false; } LogManager.i(this, "MAM support for account " + accountItem.getAccount() + " " + isSupported); supportedByAccount.put(accountItem.getAccount(), isSupported); AccountManager.getInstance().onAccountChanged(accountItem.getAccount()); return isSupported; } public void requestUpdatePreferences(final AccountJid accountJid) { Application.getInstance().runInBackgroundUserRequest(new Runnable() { @Override public void run() { AccountItem accountItem = AccountManager.getInstance().getAccount(accountJid); if (accountItem == null) { return; } org.jivesoftware.smackx.mam.MamManager mamManager = org.jivesoftware.smackx.mam.MamManager .getInstanceFor(accountItem.getConnection()); try { org.jivesoftware.smackx.mam.MamManager.MamPrefsResult result = mamManager.updateArchivingPreferences(null, null, accountItem.getMamDefaultBehaviour()); LogManager.i(LOG_TAG, "MAM default behavior updated to " + result.mamPrefs.getDefault()); } catch (SmackException.NoResponseException | XMPPException.XMPPErrorException | InterruptedException | SmackException.NotConnectedException | SmackException.NotLoggedInException e) { LogManager.exception(LOG_TAG, e); } } }); } public void requestLastHistoryByUser(final AbstractChat chat) { Application.getInstance().runInBackgroundUserRequest(new Runnable() { @Override public void run() { getLastHistory(chat); } }); } private void requestLastHistory(final AbstractChat chat) { Application.getInstance().runInBackground(new Runnable() { @Override public void run() { getLastHistory(chat); } }); } private boolean isTimeToRefreshHistory(AbstractChat chat) { return chat.getLastSyncedTime() != null && TimeUnit.MILLISECONDS.toMinutes(System.currentTimeMillis() - chat.getLastSyncedTime().getTime()) < SYNC_INTERVAL_MINUTES; } @SuppressWarnings("WeakerAccess") void getLastHistory(AbstractChat chat) { if (chat == null) { return; } if (isTimeToRefreshHistory(chat)) { return; } final AccountItem accountItem = AccountManager.getInstance().getAccount(chat.getAccount()); if (accountItem == null) { return; } XMPPTCPConnection connection = accountItem.getConnection(); if (!connection.isAuthenticated()) { return; } if (!checkSupport(accountItem)) { return; } EventBus.getDefault().post(new LastHistoryLoadStartedEvent(chat)); org.jivesoftware.smackx.mam.MamManager mamManager = org.jivesoftware.smackx.mam.MamManager.getInstanceFor(connection); String lastMessageMamId; int receivedMessagesCount; do { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); lastMessageMamId = getSyncInfo(realm, chat.getAccount(), chat.getUser()).getLastMessageMamId(); realm.close(); receivedMessagesCount = requestLastHistoryPage(mamManager, chat, lastMessageMamId); // if it was NOT the first time, and we got exactly one page, // it means that there should be more unloaded recent history } while (lastMessageMamId != null && receivedMessagesCount == PAGE_SIZE); // if it was first time receiving history, and we got less than a page // it mean that all previous history loaded if (lastMessageMamId == null && receivedMessagesCount >= 0 && receivedMessagesCount < PAGE_SIZE) { setRemoteHistoryCompletelyLoaded(chat); } EventBus.getDefault().post(new LastHistoryLoadFinishedEvent(chat)); } public void setRemoteHistoryCompletelyLoaded(AbstractChat chat) { LogManager.i(this, "setRemoteHistoryCompletelyLoaded " + chat.getUser()); Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); SyncInfo syncInfo = getSyncInfo(realm, chat.getAccount(), chat.getUser()); realm.beginTransaction(); syncInfo.setRemoteHistoryCompletelyLoaded(true); realm.commitTransaction(); realm.close(); } private int requestLastHistoryPage(org.jivesoftware.smackx.mam.MamManager mamManager, AbstractChat chat, String lastMessageMamId) { final org.jivesoftware.smackx.mam.MamManager.MamQueryResult mamQueryResult; try { if (lastMessageMamId == null) { mamQueryResult = mamManager.pageBefore(chat.getUser().getJid(), "", PAGE_SIZE); } else { mamQueryResult = mamManager.pageAfter(chat.getUser().getJid(), lastMessageMamId, PAGE_SIZE); } } catch (SmackException.NotLoggedInException | InterruptedException | SmackException.NotConnectedException | SmackException.NoResponseException | XMPPException.XMPPErrorException e) { LogManager.exception(this, e); return -1; } int receivedMessagesCount = mamQueryResult.forwardedMessages.size(); LogManager.i(this, "receivedMessagesCount " + receivedMessagesCount); chat.setLastSyncedTime(new Date(System.currentTimeMillis())); Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); updateLastHistorySyncInfo(realm, chat, mamQueryResult); syncMessages(realm, chat, getMessageItems(mamQueryResult, chat)); realm.close(); return receivedMessagesCount; } private void syncMessages(Realm realm, AbstractChat chat, final Collection<MessageItem> messagesFromServer) { if (messagesFromServer == null || messagesFromServer.isEmpty()) { return; } LogManager.i(this, "syncMessages: " + messagesFromServer.size()); RealmResults<MessageItem> localMessages = realm.where(MessageItem.class) .equalTo(MessageItem.Fields.ACCOUNT, chat.getAccount().toString()) .equalTo(MessageItem.Fields.USER, chat.getUser().toString()) .findAll(); Iterator<MessageItem> iterator = messagesFromServer.iterator(); while (iterator.hasNext()) { MessageItem remoteMessage = iterator.next(); // assume that Stanza ID could be not unique if (localMessages.where() .equalTo(MessageItem.Fields.STANZA_ID, remoteMessage.getStanzaId()) .equalTo(MessageItem.Fields.TEXT, remoteMessage.getText()) .count() > 0) { LogManager.i(this, "Sync. Removing message with same Stanza ID and text. Remote message:" + " Text: " + remoteMessage.getText() + " Timestamp: " + remoteMessage.getTimestamp() + " Delay Timestamp: " + remoteMessage.getDelayTimestamp() + " StanzaId: " + remoteMessage.getStanzaId()); iterator.remove(); continue; } Long remoteMessageDelayTimestamp = remoteMessage.getDelayTimestamp(); Long remoteMessageTimestamp = remoteMessage.getTimestamp(); RealmResults<MessageItem> sameTextMessages = localMessages.where() .equalTo(MessageItem.Fields.TEXT, remoteMessage.getText()).findAll(); if (isTimeStampSimilar(sameTextMessages, remoteMessageTimestamp)) { LogManager.i(this, "Sync. Found messages with same text and similar remote timestamp. Removing. Remote message:" + " Text: " + remoteMessage.getText() + " Timestamp: " + remoteMessage.getTimestamp() + " Delay Timestamp: " + remoteMessage.getDelayTimestamp() + " StanzaId: " + remoteMessage.getStanzaId()); iterator.remove(); continue; } if (remoteMessageDelayTimestamp != null && isTimeStampSimilar(sameTextMessages, remoteMessageDelayTimestamp)) { LogManager.i(this, "Sync. Found messages with same text and similar remote delay timestamp. Removing. Remote message:" + " Text: " + remoteMessage.getText() + " Timestamp: " + remoteMessage.getTimestamp() + " Delay Timestamp: " + remoteMessage.getDelayTimestamp() + " StanzaId: " + remoteMessage.getStanzaId()); iterator.remove(); continue; } } realm.beginTransaction(); realm.copyToRealm(messagesFromServer); realm.commitTransaction(); } private static boolean isTimeStampSimilar(RealmResults<MessageItem> sameTextMessages, long remoteMessageTimestamp) { long start = remoteMessageTimestamp - (1000 * 5); long end = remoteMessageTimestamp + (1000 * 5); if (sameTextMessages.where() .between(MessageItem.Fields.TIMESTAMP, start, end) .count() > 0) { LogManager.i(MamManager.class.getSimpleName(), "Sync. Found messages with similar local timestamp"); return true; } if (sameTextMessages.where() .between(MessageItem.Fields.DELAY_TIMESTAMP, start, end) .count() > 0) { LogManager.i(MamManager.class.getSimpleName(), "Sync. Found messages with similar local delay timestamp."); return true; } return false; } @NonNull private SyncInfo getSyncInfo(Realm realm, AccountJid account, UserJid user) { SyncInfo syncInfo = realm.where(SyncInfo.class) .equalTo(SyncInfo.FIELD_ACCOUNT, account.toString()) .equalTo(SyncInfo.FIELD_USER, user.toString()).findFirst(); if (syncInfo == null) { realm.beginTransaction(); syncInfo = realm.createObject(SyncInfo.class); syncInfo.setAccount(account); syncInfo.setUser(user); realm.commitTransaction(); } return syncInfo; } private void updateLastHistorySyncInfo(Realm realm, BaseEntity chat, org.jivesoftware.smackx.mam.MamManager.MamQueryResult mamQueryResult) { SyncInfo syncInfo = getSyncInfo(realm, chat.getAccount(), chat.getUser()); realm.beginTransaction(); if (mamQueryResult.mamFin.getRSMSet() != null) { if (syncInfo.getFirstMamMessageMamId() == null) { syncInfo.setFirstMamMessageMamId(mamQueryResult.mamFin.getRSMSet().getFirst()); if (!mamQueryResult.forwardedMessages.isEmpty()) { syncInfo.setFirstMamMessageStanzaId(mamQueryResult.forwardedMessages.get(0).getForwardedStanza().getStanzaId()); } } if (mamQueryResult.mamFin.getRSMSet().getLast() != null) { syncInfo.setLastMessageMamId(mamQueryResult.mamFin.getRSMSet().getLast()); } } realm.commitTransaction(); } public void requestPreviousHistory(final AbstractChat chat) { if (chat == null || chat.isRemotePreviousHistoryCompletelyLoaded()) { return; } final AccountItem accountItem = AccountManager.getInstance().getAccount(chat.getAccount()); if (accountItem == null || !accountItem.getFactualStatusMode().isOnline()) { return; } Application.getInstance().runInBackgroundUserRequest(new Runnable() { @Override public void run() { if (!checkSupport(accountItem)) { return; } String firstMamMessageMamId; boolean remoteHistoryCompletelyLoaded; { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); SyncInfo syncInfo = getSyncInfo(realm, chat.getAccount(), chat.getUser()); firstMamMessageMamId = syncInfo.getFirstMamMessageMamId(); remoteHistoryCompletelyLoaded = syncInfo.isRemoteHistoryCompletelyLoaded(); realm.close(); } if (remoteHistoryCompletelyLoaded) { chat.setRemotePreviousHistoryCompletelyLoaded(true); } if (firstMamMessageMamId == null || remoteHistoryCompletelyLoaded) { return; } org.jivesoftware.smackx.mam.MamManager mamManager = org.jivesoftware.smackx.mam.MamManager.getInstanceFor(accountItem.getConnection()); final org.jivesoftware.smackx.mam.MamManager.MamQueryResult mamQueryResult; try { EventBus.getDefault().post(new PreviousHistoryLoadStartedEvent(chat)); LogManager.i("MAM", "Loading previous history"); mamQueryResult = mamManager.pageBefore(chat.getUser().getJid(), firstMamMessageMamId, PAGE_SIZE); } catch (SmackException.NotLoggedInException | SmackException.NoResponseException | XMPPException.XMPPErrorException | InterruptedException | SmackException.NotConnectedException e) { LogManager.exception(this, e); EventBus.getDefault().post(new PreviousHistoryLoadFinishedEvent(chat)); return; } EventBus.getDefault().post(new PreviousHistoryLoadFinishedEvent(chat)); LogManager.i("MAM", "queryArchive finished. fin count expected: " + mamQueryResult.mamFin.getRSMSet().getCount() + " real: " + mamQueryResult.forwardedMessages.size()); Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); List<MessageItem> messageItems = getMessageItems(mamQueryResult, chat); syncMessages(realm, chat, messageItems); updatePreviousHistorySyncInfo(realm, chat, mamQueryResult, messageItems); realm.close(); } }); } private void updatePreviousHistorySyncInfo(Realm realm, BaseEntity chat, org.jivesoftware.smackx.mam.MamManager.MamQueryResult mamQueryResult, List<MessageItem> messageItems) { SyncInfo syncInfo = getSyncInfo(realm, chat.getAccount(), chat.getUser()); realm.beginTransaction(); if (mamQueryResult.forwardedMessages.size() < PAGE_SIZE) { syncInfo.setRemoteHistoryCompletelyLoaded(true); } syncInfo.setFirstMamMessageMamId(mamQueryResult.mamFin.getRSMSet().getFirst()); if (!mamQueryResult.forwardedMessages.isEmpty()) { syncInfo.setFirstMamMessageStanzaId(mamQueryResult.forwardedMessages.get(0).getForwardedStanza().getStanzaId()); } realm.commitTransaction(); } private List<MessageItem> getMessageItems(org.jivesoftware.smackx.mam.MamManager.MamQueryResult mamQueryResult, AbstractChat chat) { List<MessageItem> messageItems = new ArrayList<>(); for (Forwarded forwarded : mamQueryResult.forwardedMessages) { if (!(forwarded.getForwardedStanza() instanceof Message)) { continue; } Message message = (Message) forwarded.getForwardedStanza(); DelayInformation delayInformation = forwarded.getDelayInformation(); DelayInformation messageDelay = DelayInformation.from(message); String body = message.getBody(); net.java.otr4j.io.messages.AbstractMessage otrMessage; try { otrMessage = SerializationUtils.toMessage(body); } catch (IOException e) { return null; } if (otrMessage != null) { if (otrMessage.messageType != net.java.otr4j.io.messages.AbstractMessage.MESSAGE_PLAINTEXT) return null; body = ((PlainTextMessage) otrMessage).cleanText; } boolean incoming = message.getFrom().asBareJid().equals(chat.getUser().getJid().asBareJid()); MessageItem messageItem = new MessageItem(); messageItem.setAccount(chat.getAccount()); messageItem.setUser(chat.getUser()); messageItem.setResource(chat.getUser().getJid().getResourceOrNull()); messageItem.setText(body); messageItem.setTimestamp(delayInformation.getStamp().getTime()); if (messageDelay != null) { messageItem.setDelayTimestamp(messageDelay.getStamp().getTime()); } messageItem.setIncoming(incoming); messageItem.setStanzaId(message.getStanzaId()); messageItem.setReceivedFromMessageArchive(true); messageItem.setRead(true); messageItem.setSent(true); FileManager.processFileMessage(messageItem); messageItems.add(messageItem); } return messageItems; } }