/** * 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.support.annotation.NonNull; import android.support.annotation.Nullable; import com.xabber.android.data.Application; import com.xabber.android.data.NetworkException; import com.xabber.android.data.SettingsManager; import com.xabber.android.data.connection.StanzaSender; 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.carbons.CarbonManager; import com.xabber.android.data.extension.cs.ChatStateManager; import com.xabber.android.data.extension.file.FileManager; import com.xabber.android.data.message.chat.ChatManager; import com.xabber.android.data.notification.NotificationManager; import org.greenrobot.eventbus.EventBus; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.packet.Message; import org.jivesoftware.smack.packet.Message.Type; import org.jivesoftware.smack.packet.Stanza; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.delay.packet.DelayInformation; import org.jxmpp.jid.Jid; import org.jxmpp.jid.parts.Resourcepart; import java.io.File; import java.util.Date; import java.util.UUID; import io.realm.Realm; import io.realm.RealmChangeListener; import io.realm.RealmResults; import io.realm.Sort; /** * Chat instance. * * @author alexander.ivanov */ public abstract class AbstractChat extends BaseEntity implements RealmChangeListener<RealmResults<MessageItem>> { /** * Number of messages from history to be shown for context purpose. */ public static final int PRELOADED_MESSAGES = 50; /** * Whether chat is open and should be displayed as active chat. */ protected boolean active; /** * Whether changes in status should be record. */ private boolean trackStatus; /** * Whether user never received notifications from this chat. */ private boolean firstNotification; /** * Current thread id. */ private String threadId; private boolean isPrivateMucChat; private boolean isPrivateMucChatAccepted; private boolean isRemotePreviousHistoryCompletelyLoaded = false; private Date lastSyncedTime; private RealmResults<SyncInfo> syncInfo; private MessageItem lastMessage; private RealmResults<MessageItem> messages; protected AbstractChat(@NonNull final AccountJid account, @NonNull final UserJid user, boolean isPrivateMucChat) { super(account, isPrivateMucChat ? user : user.getBareUserJid()); threadId = StringUtils.randomString(12); active = false; trackStatus = false; firstNotification = true; this.isPrivateMucChat = isPrivateMucChat; isPrivateMucChatAccepted = false; Application.getInstance().runOnUiThread(new Runnable() { @Override public void run() { getMessages(); } }); } public boolean isRemotePreviousHistoryCompletelyLoaded() { return isRemotePreviousHistoryCompletelyLoaded; } public void setRemotePreviousHistoryCompletelyLoaded(boolean remotePreviousHistoryCompletelyLoaded) { isRemotePreviousHistoryCompletelyLoaded = remotePreviousHistoryCompletelyLoaded; } public Date getLastSyncedTime() { return lastSyncedTime; } public void setLastSyncedTime(Date lastSyncedTime) { this.lastSyncedTime = lastSyncedTime; } public boolean isActive() { if (isPrivateMucChat && !isPrivateMucChatAccepted) { return false; } return active; } public void openChat() { active = true; trackStatus = true; } void closeChat() { active = false; firstNotification = true; } private String getAccountString() { return account.toString(); } private String getUserString() { return user.toString(); } public RealmResults<MessageItem> getMessages() { if (messages == null) { messages = MessageDatabaseManager.getChatMessages( MessageDatabaseManager.getInstance().getRealmUiThread(), account, user); updateLastMessage(); messages.addChangeListener(this); } return messages; } public RealmResults<SyncInfo> getSyncInfo() { if (syncInfo == null) { syncInfo = MessageDatabaseManager.getInstance() .getRealmUiThread().where(SyncInfo.class) .equalTo(SyncInfo.FIELD_ACCOUNT, getAccountString()) .equalTo(SyncInfo.FIELD_USER, getUserString()) .findAllAsync(); } return syncInfo; } boolean isStatusTrackingEnabled() { return trackStatus; } /** * @return Target address for sending message. */ @NonNull public abstract Jid getTo(); /** * @return Message type to be assigned. */ public abstract Type getType(); /** * @return Whether user never received notifications from this chat. And * mark as received. */ public boolean getFirstNotification() { boolean result = firstNotification; firstNotification = false; return result; } /** * @return Whether user should be notified about incoming messages in chat. */ protected boolean notifyAboutMessage() { return SettingsManager.eventsMessage() != SettingsManager.EventsMessage.none; } abstract protected MessageItem createNewMessageItem(String text); /** * Creates new action. * @param resource can be <code>null</code>. * @param text can be <code>null</code>. */ public void newAction(Resourcepart resource, String text, ChatAction action) { createAndSaveNewMessage(resource, text, action, null, true, false, false, false, null); } /** * Creates new message. * <p/> * Any parameter can be <code>null</code> (except boolean values). * * @param resource Contact's resource or nick in conference. * @param text message. * @param action Informational message. * @param delayTimestamp Time when incoming message was sent or outgoing was created. * @param incoming Incoming message. * @param notify Notify user about this message when appropriated. * @param unencrypted Whether not encrypted message in OTR chat was received. * @param offline Whether message was received from server side offline storage. * @return */ protected void createAndSaveNewMessage(Resourcepart resource, String text, final ChatAction action, final Date delayTimestamp, final boolean incoming, boolean notify, final boolean unencrypted, final boolean offline, final String stanzaId) { final MessageItem messageItem = createMessageItem(resource, text, action, delayTimestamp, incoming, notify, unencrypted, offline, stanzaId); saveMessageItem(messageItem); EventBus.getDefault().post(new NewMessageEvent()); } public void saveMessageItem(final MessageItem messageItem) { MessageDatabaseManager.getInstance().getRealmUiThread() .executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { realm.copyToRealm(messageItem); } }); } protected MessageItem createMessageItem(Resourcepart resource, String text, ChatAction action, Date delayTimestamp, boolean incoming, boolean notify, boolean unencrypted, boolean offline, String stanzaId) { final boolean visible = MessageManager.getInstance().isVisibleChat(this); boolean read = incoming ? visible : true; boolean send = incoming; if (action == null && text == null) { throw new IllegalArgumentException(); } if (text == null) { text = ""; } if (action != null) { read = true; send = true; } final Date timestamp = new Date(); if (text.trim().isEmpty()) { notify = false; } if (notify || !incoming) { openChat(); } if (!incoming) { notify = false; } if (isPrivateMucChat) { if (!isPrivateMucChatAccepted) { notify = false; } } MessageItem messageItem = new MessageItem(); messageItem.setAccount(account); messageItem.setUser(user); if (resource == null) { messageItem.setResource(Resourcepart.EMPTY); } else { messageItem.setResource(resource); } if (action != null) { messageItem.setAction(action.toString()); } messageItem.setText(text); messageItem.setTimestamp(timestamp.getTime()); if (delayTimestamp != null) { messageItem.setDelayTimestamp(delayTimestamp.getTime()); } messageItem.setIncoming(incoming); messageItem.setRead(read); messageItem.setSent(send); messageItem.setUnencrypted(unencrypted); messageItem.setOffline(offline); messageItem.setStanzaId(stanzaId); FileManager.processFileMessage(messageItem); if (notify && notifyAboutMessage()) { if (visible) { if (ChatManager.getInstance().isNotifyVisible(account, user)) { NotificationManager.getInstance().onMessageNotification(messageItem); } } else { NotificationManager.getInstance().onMessageNotification(messageItem); } } return messageItem; } String newFileMessage(final File file) { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); final String messageId = UUID.randomUUID().toString(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { MessageItem messageItem = new MessageItem(messageId); messageItem.setAccount(account); messageItem.setUser(user); messageItem.setText(file.getName()); messageItem.setFilePath(file.getPath()); messageItem.setIsImage(FileManager.fileIsImage(file)); messageItem.setTimestamp(System.currentTimeMillis()); messageItem.setRead(true); messageItem.setSent(true); messageItem.setError(false); messageItem.setIncoming(false); messageItem.setInProgress(true); realm.copyToRealm(messageItem); } }); realm.close(); return messageId; } /** * @return Whether chat accepts packets from specified user. */ private boolean accept(UserJid jid) { return this.user.equals(jid); } @Nullable public synchronized MessageItem getLastMessage() { return lastMessage; } private void updateLastMessage() { if (messages.isValid() && messages.isLoaded() && !messages.isEmpty()) { lastMessage = MessageDatabaseManager.getInstance() .getRealmUiThread() .copyFromRealm(messages.last()); } else { lastMessage = null; } } /** * @return Time of last message in chat. Can be <code>null</code>. */ public Date getLastTime() { MessageItem lastMessage = getLastMessage(); if (lastMessage != null) { return new Date(lastMessage.getTimestamp()); } else { return null; } } /** * @return New message packet to be sent. */ public Message createMessagePacket(String body) { Message message = new Message(); message.setTo(getTo()); message.setType(getType()); message.setBody(body); message.setThread(threadId); return message; } /** * Prepare text to be send. * * @return <code>null</code> if text shouldn't be send. */ protected String prepareText(String text) { return text; } public void sendMessages() { Application.getInstance().runInBackgroundUserRequest(new Runnable() { @Override public void run() { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); RealmResults<MessageItem> messagesToSend = realm.where(MessageItem.class) .equalTo(MessageItem.Fields.ACCOUNT, account.toString()) .equalTo(MessageItem.Fields.USER, user.toString()) .equalTo(MessageItem.Fields.SENT, false) .findAllSorted(MessageItem.Fields.TIMESTAMP, Sort.ASCENDING); realm.beginTransaction(); for (final MessageItem messageItem : messagesToSend) { if (!sendMessage(messageItem)) { break; } } realm.commitTransaction(); realm.close(); } }); } @SuppressWarnings("WeakerAccess") boolean sendMessage(MessageItem messageItem) { String text = prepareText(messageItem.getText()); Long timestamp = messageItem.getTimestamp(); Date currentTime = new Date(System.currentTimeMillis()); Date delayTimestamp = null; if (timestamp != null) { if (currentTime.getTime() - timestamp > 60000) { delayTimestamp = currentTime; } } Message message = null; if (text != null) { message = createMessagePacket(text); } if (message != null) { ChatStateManager.getInstance().updateOutgoingMessage(AbstractChat.this, message); CarbonManager.getInstance().updateOutgoingMessage(AbstractChat.this, message); if (delayTimestamp != null) { message.addExtension(new DelayInformation(delayTimestamp)); } final String messageId = messageItem.getUniqueId(); try { StanzaSender.sendStanza(account, message, new StanzaListener() { @Override public void processStanza(Stanza packet) throws SmackException.NotConnectedException { Realm realm = MessageDatabaseManager.getInstance().getNewBackgroundRealm(); realm.executeTransaction(new Realm.Transaction() { @Override public void execute(Realm realm) { MessageItem acknowledgedMessage = realm .where(MessageItem.class) .equalTo(MessageItem.Fields.UNIQUE_ID, messageId) .findFirst(); if (acknowledgedMessage != null) { acknowledgedMessage.setAcknowledged(true); } } }); realm.close(); } }); } catch (NetworkException e) { return false; } } if (message == null) { messageItem.setError(true); } else { messageItem.setStanzaId(message.getStanzaId()); } if (delayTimestamp != null) { messageItem.setDelayTimestamp(delayTimestamp.getTime()); } if (messageItem.getTimestamp() == null) { messageItem.setTimestamp(currentTime.getTime()); } messageItem.setSent(true); return true; } public String getThreadId() { return threadId; } /** * Update thread id with new value. * * @param threadId <code>null</code> if current value shouldn't be changed. */ protected void updateThreadId(String threadId) { if (threadId == null) { return; } this.threadId = threadId; } /** * Processes incoming packet. * * @param userJid * @param packet * @return Whether packet was directed to this chat. */ protected boolean onPacket(UserJid userJid, Stanza packet) { return accept(userJid); } /** * Connection complete.f */ protected void onComplete() { } /** * Disconnection occured. */ protected void onDisconnect() { } public void setIsPrivateMucChatAccepted(boolean isPrivateMucChatAccepted) { this.isPrivateMucChatAccepted = isPrivateMucChatAccepted; } boolean isPrivateMucChat() { return isPrivateMucChat; } boolean isPrivateMucChatAccepted() { return isPrivateMucChatAccepted; } @Override public void onChange(RealmResults<MessageItem> messageItems) { updateLastMessage(); } }