/** * Copyright (c) 2013, Redsolution LTD. All rights reserved. * * This file is part of Xabber project; you can redistribute it and/or * modify it under the terms of the GNU General Public License, Version 3. * * Xabber is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License, * along with this program. If not, see http://www.gnu.org/licenses/. */ package com.xabber.android.data.message; import android.os.Environment; import android.support.annotation.Nullable; import com.xabber.android.R; import com.xabber.android.data.Application; import com.xabber.android.data.NetworkException; import com.xabber.android.data.OnLoadListener; import com.xabber.android.data.SettingsManager; import com.xabber.android.data.SettingsManager.ChatsShowStatusChange; import com.xabber.android.data.account.AccountItem; import com.xabber.android.data.account.AccountManager; import com.xabber.android.data.account.StatusMode; import com.xabber.android.data.account.listeners.OnAccountDisabledListener; import com.xabber.android.data.account.listeners.OnAccountRemovedListener; import com.xabber.android.data.connection.ConnectionItem; import com.xabber.android.data.connection.listeners.OnDisconnectListener; import com.xabber.android.data.connection.listeners.OnPacketListener; import com.xabber.android.data.database.MessageDatabaseManager; import com.xabber.android.data.database.messagerealm.MessageItem; import com.xabber.android.data.entity.AccountJid; import com.xabber.android.data.entity.BaseEntity; import com.xabber.android.data.entity.NestedMap; import com.xabber.android.data.entity.UserJid; import com.xabber.android.data.extension.muc.MUCManager; import com.xabber.android.data.extension.muc.RoomChat; import com.xabber.android.data.log.LogManager; import com.xabber.android.data.message.chat.MucPrivateChatNotification; import com.xabber.android.data.notification.EntityNotificationProvider; import com.xabber.android.data.notification.NotificationManager; import com.xabber.android.data.roster.OnRosterReceivedListener; import com.xabber.android.data.roster.OnStatusChangeListener; import com.xabber.android.data.roster.RosterManager; import com.xabber.android.utils.StringUtils; import org.greenrobot.eventbus.EventBus; import org.jivesoftware.smack.packet.ExtensionElement; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smackx.carbons.packet.CarbonExtension; import org.jivesoftware.smackx.muc.packet.MUCUser; import org.jxmpp.jid.FullJid; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import io.realm.Realm; import io.realm.RealmResults; /** * Manage chats and its messages. * <p/> * Warning: message processing using chat instances should be changed. * * @author alexander.ivanov */ public class MessageManager implements OnLoadListener, OnPacketListener, OnDisconnectListener, OnAccountRemovedListener, OnAccountDisabledListener, OnRosterReceivedListener, OnStatusChangeListener { private static MessageManager instance; private final EntityNotificationProvider<MucPrivateChatNotification> mucPrivateChatRequestProvider; /** * Registered chats for bareAddresses in accounts. */ private final NestedMap<AbstractChat> chats; /** * Visible chat. * <p/> * Will be <code>null</code> if there is no one. */ private AbstractChat visibleChat; public static MessageManager getInstance() { if (instance == null) { instance = new MessageManager(); } return instance; } private MessageManager() { chats = new NestedMap<>(); mucPrivateChatRequestProvider = new EntityNotificationProvider<> (R.drawable.ic_stat_muc_private_chat_request_white_24dp); mucPrivateChatRequestProvider.setCanClearNotifications(false); } @Override public void onLoad() { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { RealmResults<MessageItem> messagesToSend = realm.where(MessageItem.class) .equalTo(MessageItem.Fields.SENT, false) .findAll(); for (MessageItem messageItem : messagesToSend) { AccountJid account = messageItem.getAccount(); UserJid user = messageItem.getUser(); if (account != null && user != null) { if (getChat(account, user) == null) { createChat(account, user); } } } } }); realm.close(); NotificationManager.getInstance().registerNotificationProvider(mucPrivateChatRequestProvider); } /** * @return <code>null</code> if there is no such chat. */ @Nullable public AbstractChat getChat(AccountJid account, UserJid user) { if (account != null && user != null) { return chats.get(account.toString(), user.getBareJid().toString()); } else { return null; } } public Collection<AbstractChat> getChats() { List<AbstractChat> chats = new ArrayList<>(); for (AccountJid accountJid : AccountManager.getInstance().getAllAccounts()) { chats.addAll(this.chats.getNested(accountJid.toString()).values()); } return chats; } /** * Creates and adds new regular chat to be managed. * * @param account * @param user * @return */ private RegularChat createChat(AccountJid account, UserJid user) { RegularChat chat = new RegularChat(account, user, false); addChat(chat); return chat; } private RegularChat createPrivateMucChat(AccountJid account, FullJid fullJid) throws UserJid.UserJidCreateException { RegularChat chat = new RegularChat(account, UserJid.from(fullJid), true); addChat(chat); return chat; } /** * Adds chat to be managed. * * @param chat */ public void addChat(AbstractChat chat) { if (getChat(chat.getAccount(), chat.getUser()) != null) { throw new IllegalStateException(); } chats.put(chat.getAccount().toString(), chat.getUser().toString(), chat); } /** * Removes chat from managed. * * @param chat */ public void removeChat(AbstractChat chat) { chat.closeChat(); LogManager.i(this, "removeChat " + chat.getUser()); chats.remove(chat.getAccount().toString(), chat.getUser().toString()); } /** * Sends message. Creates and registers new chat if necessary. * * @param account * @param user * @param text */ public void sendMessage(AccountJid account, UserJid user, String text) { AbstractChat chat = getOrCreateChat(account, user); sendMessage(text, chat); } private void sendMessage(final String text, final AbstractChat chat) { MessageDatabaseManager.getInstance() .getRealmUiThread().executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { MessageItem newMessageItem = chat.createNewMessageItem(text); realm.copyToRealm(newMessageItem); } }); chat.sendMessages(); } public String createFileMessage(AccountJid account, UserJid user, File file) { AbstractChat chat = getOrCreateChat(account, user); chat.openChat(); return chat.newFileMessage(file); } public void updateFileMessage(AccountJid account, UserJid user, final String messageId, final String url) { final AbstractChat chat = getChat(account, user); if (chat == null) { return; } Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { MessageItem messageItem = realm.where(MessageItem.class) .equalTo(MessageItem.Fields.UNIQUE_ID, messageId) .findFirst(); if (messageItem != null) { messageItem.setText(url); messageItem.setSent(false); messageItem.setInProgress(false); } } }); realm.close(); chat.sendMessages(); } public void updateMessageWithError(final String messageId) { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { MessageItem messageItem = realm.where(MessageItem.class) .equalTo(MessageItem.Fields.UNIQUE_ID, messageId) .findFirst(); if (messageItem != null) { messageItem.setError(true); messageItem.setInProgress(false); } } }); realm.close(); } /** * @param account * @param user * @return Where there is active chat. */ public boolean hasActiveChat(AccountJid account, UserJid user) { AbstractChat chat = getChat(account, user); return chat != null && chat.isActive(); } /** * @return Collection with active chats. */ public Collection<AbstractChat> getActiveChats() { Collection<AbstractChat> collection = new ArrayList<>(); for (AbstractChat chat : chats.values()) { if (chat.isActive()) { collection.add(chat); } } return Collections.unmodifiableCollection(collection); } /** * Returns existed chat or create new one. * */ public AbstractChat getOrCreateChat(AccountJid account, UserJid user) { if (MUCManager.getInstance().isMucPrivateChat(account, user)) { try { return getOrCreatePrivateMucChat(account, user.getJid().asFullJidIfPossible()); } catch (UserJid.UserJidCreateException e) { return null; } } AbstractChat chat = getChat(account, user); if (chat == null) { chat = createChat(account, user); } return chat; } public AbstractChat getOrCreatePrivateMucChat(AccountJid account, FullJid fullJid) throws UserJid.UserJidCreateException { AbstractChat chat = getChat(account, UserJid.from(fullJid)); if (chat == null) { chat = createPrivateMucChat(account, fullJid); } return chat; } /** * Force open chat (make it active). * * @param account * @param user */ public void openChat(AccountJid account, UserJid user) { getOrCreateChat(account, user).openChat(); } public void openPrivateMucChat(AccountJid account, FullJid fullJid) throws UserJid.UserJidCreateException { getOrCreatePrivateMucChat(account, fullJid).openChat(); } /** * Closes specified chat (make it inactive). * * @param account * @param user */ public void closeChat(AccountJid account, UserJid user) { AbstractChat chat = getChat(account, user); if (chat == null) { return; } chat.closeChat(); } /** * Sets currently visible chat. */ public void setVisibleChat(BaseEntity visibleChat) { AbstractChat chat = getChat(visibleChat.getAccount(), visibleChat.getUser()); if (chat == null) { chat = createChat(visibleChat.getAccount(), visibleChat.getUser()); } else { final AccountJid account = chat.getAccount(); final UserJid user = chat.getUser(); MessageDatabaseManager.getInstance() .getRealmUiThread().executeTransactionAsync(new Realm.Transaction() { @Override public void execute(Realm realm) { RealmResults<MessageItem> unreadMessages = realm.where(MessageItem.class) .equalTo(MessageItem.Fields.ACCOUNT, account.toString()) .equalTo(MessageItem.Fields.USER, user.toString()) .equalTo(MessageItem.Fields.READ, false) .findAll(); List<MessageItem> unreadMessagesList = new ArrayList<>(unreadMessages); for (MessageItem messageItem : unreadMessagesList) { messageItem.setRead(true); } } }); } this.visibleChat = chat; } /** * All chats become invisible. */ public void removeVisibleChat() { visibleChat = null; } /** * @param chat * @return Whether specified chat is currently visible. */ boolean isVisibleChat(AbstractChat chat) { return visibleChat == chat; } /** * Removes all messages from chat. * * @param account * @param user */ public void clearHistory(final AccountJid account, final UserJid user) { MessageDatabaseManager.getInstance() .getRealmUiThread().executeTransactionAsync(new Realm.Transaction() { @Override public void execute(Realm realm) { realm.where(MessageItem.class) .equalTo(MessageItem.Fields.ACCOUNT, account.toString()) .equalTo(MessageItem.Fields.USER, user.toString()) .findAll().deleteAllFromRealm(); } }); } /** * Removes message from history. * */ public void removeMessage(final String messageItemId) { Application.getInstance().runInBackgroundUserRequest(new Runnable() { @Override public void run() { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); MessageItem messageItem = realm.where(MessageItem.class) .equalTo(MessageItem.Fields.UNIQUE_ID, messageItemId).findFirst(); if (messageItem != null) { realm.beginTransaction(); messageItem.deleteFromRealm(); realm.commitTransaction(); } realm.close(); } }); } /** * Called on action settings change. */ public void onSettingsChanged() { } @Override public void onStanza(ConnectionItem connection, Stanza stanza) { if (stanza.getFrom() == null) { return; } AccountJid account = connection.getAccount(); final UserJid user; try { user = UserJid.from(stanza.getFrom()).getBareUserJid(); } catch (UserJid.UserJidCreateException e) { return; } boolean processed = false; for (AbstractChat chat : chats.getNested(account.toString()).values()) { if (chat.onPacket(user, stanza)) { processed = true; break; } } final AbstractChat chat = getChat(account, user); if (chat != null && stanza instanceof Message) { if (chat.isPrivateMucChat() && !chat.isPrivateMucChatAccepted()) { if (mucPrivateChatRequestProvider.get(chat.getAccount(), chat.getUser()) == null) { mucPrivateChatRequestProvider.add(new MucPrivateChatNotification(account, user), true); } } return; } if (!processed && stanza instanceof Message) { final Message message = (Message) stanza; final String body = message.getBody(); if (body == null) { return; } if (message.getType() == Message.Type.chat && MUCManager.getInstance().hasRoom(account, user.getJid().asEntityBareJidIfPossible())) { try { createPrivateMucChat(account, user.getJid().asFullJidIfPossible()).onPacket(user, stanza); } catch (UserJid.UserJidCreateException e) { LogManager.exception(this, e); } mucPrivateChatRequestProvider.add(new MucPrivateChatNotification(account, user), true); return; } for (ExtensionElement packetExtension : message.getExtensions()) { if (packetExtension instanceof MUCUser) { return; } } createChat(account, user).onPacket(user, stanza); } } public void processCarbonsMessage(AccountJid account, final Message message, CarbonExtension.Direction direction) { if (direction == CarbonExtension.Direction.sent) { UserJid companion; try { companion = UserJid.from(message.getTo()).getBareUserJid(); } catch (UserJid.UserJidCreateException e) { return; } AbstractChat chat = getChat(account, companion); if (chat == null) { chat = createChat(account, companion); } final String body = message.getBody(); if (body == null) { return; } final AbstractChat finalChat = chat; MessageDatabaseManager.getInstance().getRealmUiThread().executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { MessageItem newMessageItem = finalChat.createNewMessageItem(body); newMessageItem.setStanzaId(message.getStanzaId()); newMessageItem.setSent(true); newMessageItem.setForwarded(true); realm.copyToRealm(newMessageItem); } }); EventBus.getDefault().post(new NewMessageEvent()); return; } UserJid companion = null; try { companion = UserJid.from(message.getFrom()).getBareUserJid(); } catch (UserJid.UserJidCreateException e) { return; } boolean processed = false; for (AbstractChat chat : chats.getNested(account.toString()).values()) { if (chat.onPacket(companion, message)) { processed = true; break; } } if (getChat(account, companion) != null) { return; } if (processed) { return; } final String body = message.getBody(); if (body == null) { return; } createChat(account, companion).onPacket(companion, message); } @Override public void onRosterReceived(AccountItem accountItem) { for (AbstractChat chat : chats.getNested(accountItem.getAccount().toString()).values()) { chat.onComplete(); } } @Override public void onDisconnect(ConnectionItem connection) { if (!(connection instanceof AccountItem)) { return; } AccountJid account = connection.getAccount(); for (AbstractChat chat : chats.getNested(account.toString()).values()) { chat.onDisconnect(); } } @Override public void onAccountRemoved(AccountItem accountItem) { chats.clear(accountItem.getAccount().toString()); } @Override public void onAccountDisabled(AccountItem accountItem) { chats.clear(accountItem.getAccount().toString()); } /** * Export chat to file with specified name. * * @param account * @param user * @param fileName * @throws NetworkException */ public File exportChat(AccountJid account, UserJid user, String fileName) throws NetworkException { final File file = new File(Environment.getExternalStorageDirectory(), fileName); try { BufferedWriter out = new BufferedWriter(new FileWriter(file)); final String titleName = RosterManager.getInstance().getName(account, user) + " (" + user + ")"; out.write("<html><head><title>"); out.write(StringUtils.escapeHtml(titleName)); out.write("</title></head><body>"); final AbstractChat abstractChat = getChat(account, user); if (abstractChat != null) { final boolean isMUC = abstractChat instanceof RoomChat; final String accountName = AccountManager.getInstance().getNickName(account); final String userName = RosterManager.getInstance().getName(account, user); Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); RealmResults<MessageItem> messageItems = MessageDatabaseManager.getChatMessages(realm, account, user); for (MessageItem messageItem : messageItems) { if (messageItem.getAction() != null) { continue; } final String name; if (isMUC) { name = messageItem.getResource().toString(); } else { if (messageItem.isIncoming()) { name = userName; } else { name = accountName; } } out.write("<b>"); out.write(StringUtils.escapeHtml(name)); out.write("</b> ("); out.write(StringUtils.getDateTimeText(new Date(messageItem.getTimestamp()))); out.write(")<br />\n<p>"); out.write(StringUtils.escapeHtml(messageItem.getText())); out.write("</p><hr />\n"); } realm.close(); } out.write("</body></html>"); out.close(); } catch (IOException e) { throw new NetworkException(R.string.FILE_NOT_FOUND); } return file; } private boolean isStatusTrackingEnabled(AccountJid account, UserJid user) { if (SettingsManager.chatsShowStatusChange() != ChatsShowStatusChange.always) { return false; } AbstractChat abstractChat = getChat(account, user); return abstractChat != null && abstractChat instanceof RegularChat && abstractChat.isStatusTrackingEnabled(); } @Override public void onStatusChanged(AccountJid account, UserJid user, String statusText) { if (isStatusTrackingEnabled(account, user)) { AbstractChat chat = getChat(account, user); if (chat != null) { chat.newAction(user.getJid().getResourceOrNull(), statusText, ChatAction.status); } } } @Override public void onStatusChanged(AccountJid account, UserJid user, StatusMode statusMode, String statusText) { if (isStatusTrackingEnabled(account, user)) { AbstractChat chat = getChat(account, user); if (chat != null) { chat.newAction(user.getJid().getResourceOrNull(), statusText, ChatAction.getChatAction(statusMode)); } } } public void acceptMucPrivateChat(AccountJid account, UserJid user) throws UserJid.UserJidCreateException { mucPrivateChatRequestProvider.remove(account, user); getOrCreatePrivateMucChat(account, user.getJid().asFullJidIfPossible()).setIsPrivateMucChatAccepted(true); } public void discardMucPrivateChat(AccountJid account, UserJid user) { mucPrivateChatRequestProvider.remove(account, user); } public static void closeActiveChats() { for (AbstractChat chat : MessageManager.getInstance().getActiveChats()) { MessageManager.getInstance().closeChat(chat.getAccount(), chat.getUser()); NotificationManager.getInstance(). removeMessageNotification(chat.getAccount(), chat.getUser()); } } }