/**
* 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.extension.muc;
import android.support.annotation.NonNull;
import com.xabber.android.R;
import com.xabber.android.data.Application;
import com.xabber.android.data.SettingsManager;
import com.xabber.android.data.SettingsManager.ChatsShowStatusChange;
import com.xabber.android.data.account.StatusMode;
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.UserJid;
import com.xabber.android.data.log.LogManager;
import com.xabber.android.data.message.AbstractChat;
import com.xabber.android.data.message.ChatAction;
import com.xabber.android.data.message.chat.ChatManager;
import com.xabber.android.data.roster.RosterManager;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Message.Type;
import org.jivesoftware.smack.packet.Presence;
import org.jivesoftware.smack.packet.Stanza;
import org.jivesoftware.smackx.delay.packet.DelayInformation;
import org.jivesoftware.smackx.muc.MUCAffiliation;
import org.jivesoftware.smackx.muc.MUCRole;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.jivesoftware.smackx.muc.packet.MUCItem;
import org.jivesoftware.smackx.muc.packet.MUCUser;
import org.jxmpp.jid.EntityBareJid;
import org.jxmpp.jid.parts.Resourcepart;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import io.realm.Realm;
/**
* Chat room.
* <p/>
* Warning: We are going to remove SMACK components.
*
* @author alexander.ivanov
*/
public class RoomChat extends AbstractChat {
/**
* Information about occupants for STRING-PREPed resource.
*/
private final Map<Resourcepart, Occupant> occupants;
/**
* Invited user for the sent packet ID.
*/
private final Map<String, UserJid> invites;
/**
* Joining was requested from the UI.
*/
private boolean requested;
/**
* Nickname used in the room.
*/
private Resourcepart nickname;
private String password;
private RoomState state;
private String subject;
/**
* SMACK MUC implementation.
*/
private MultiUserChat multiUserChat;
public static RoomChat create(AccountJid account, EntityBareJid user, Resourcepart nickname, String password) throws UserJid.UserJidCreateException {
return new RoomChat(account, UserJid.from(user), nickname, password);
}
private RoomChat(AccountJid account, UserJid user, Resourcepart nickname, String password) {
super(account, user, false);
this.nickname = nickname;
this.password = password;
requested = false;
state = RoomState.unavailable;
subject = "";
multiUserChat = null;
occupants = new HashMap<>();
invites = new HashMap<>();
}
@NonNull
@Override
public EntityBareJid getTo() {
return getRoom();
}
@Override
public Type getType() {
return Type.groupchat;
}
EntityBareJid getRoom() {
return user.getJid().asEntityBareJidIfPossible();
}
Resourcepart getNickname() {
return nickname;
}
void setNickname(Resourcepart nickname) {
this.nickname = nickname;
}
String getPassword() {
return password;
}
void setPassword(String password) {
this.password = password;
}
boolean isRequested() {
return requested;
}
void setRequested(boolean requested) {
this.requested = requested;
}
public RoomState getState() {
return state;
}
void setState(RoomState state) {
this.state = state;
if (!state.inUse()) {
multiUserChat = null;
occupants.clear();
invites.clear();
}
if (state == RoomState.available) {
sendMessages();
}
}
Collection<Occupant> getOccupants() {
return Collections.unmodifiableCollection(occupants.values());
}
String getSubject() {
return subject;
}
public MultiUserChat getMultiUserChat() {
return multiUserChat;
}
void setMultiUserChat(MultiUserChat multiUserChat) {
this.multiUserChat = multiUserChat;
}
void putInvite(String packetID, UserJid user) {
invites.put(packetID, user);
}
@Override
protected MessageItem createNewMessageItem(String text) {
return createMessageItem(nickname, text, null, null, false, false, false, false, null);
}
@Override
protected boolean notifyAboutMessage() {
return SettingsManager.eventsMessage() == SettingsManager.EventsMessage.chatAndMuc;
}
@Override
protected boolean onPacket(UserJid bareAddress, Stanza stanza) {
if (!super.onPacket(bareAddress, stanza)) {
return false;
}
MUCUser mucUserExtension = MUCUser.from(stanza);
if (mucUserExtension != null && mucUserExtension.getInvite() != null) {
return false;
}
final org.jxmpp.jid.Jid from = stanza.getFrom();
final Resourcepart resource = from.getResourceOrNull();
if (stanza instanceof Message) {
final Message message = (Message) stanza;
if (message.getType() == Message.Type.error) {
UserJid invite = invites.remove(message.getStanzaId());
if (invite != null) {
newAction(nickname, invite.toString(), ChatAction.invite_error);
}
return true;
}
MUCUser mucUser = MUCUser.from(stanza);
if (mucUser != null && mucUser.getDecline() != null) {
onInvitationDeclined(mucUser.getDecline().getFrom(), mucUser.getDecline().getReason());
return true;
}
if (mucUser != null && mucUser.getStatus() != null && mucUser.getStatus().contains(MUCUser.Status.create("100"))
&& ChatManager.getInstance().isSuppress100(account, user)) {
// 'This room is not anonymous'
return true;
}
final String text = message.getBody();
final String subject = message.getSubject();
if (text == null && subject == null) {
return true;
}
if (subject != null) {
if (this.subject.equals(subject)) {
return true;
}
this.subject = subject;
RosterManager.onContactChanged(account, bareAddress);
newAction(resource, subject, ChatAction.subject);
} else {
boolean notify = true;
String stanzaId = message.getStanzaId();
DelayInformation delayInformation = DelayInformation.from(message);
Date delay = null;
if (delayInformation != null) {
delay = delayInformation.getStamp();
}
if (delay != null) {
notify = false;
}
Realm realm = MessageDatabaseManager.getInstance().getRealmUiThread();
final MessageItem sameMessage = realm
.where(MessageItem.class)
.equalTo(MessageItem.Fields.STANZA_ID, stanzaId)
.findFirst();
// Server send our own message back
if (sameMessage != null) {
realm.beginTransaction();
sameMessage.setDelivered(true);
realm.commitTransaction();
return true;
}
if (isSelf(resource)) { // Own message from other client
notify = false;
}
updateThreadId(message.getThread());
createAndSaveNewMessage(resource, text, null, delay, true, notify, false, false, message.getStanzaId());
}
} else if (stanza instanceof Presence) {
Presence presence = (Presence) stanza;
if (presence.getType() == Presence.Type.available) {
Occupant oldOccupant = occupants.get(resource);
Occupant newOccupant = createOccupant(resource, presence);
occupants.put(resource, newOccupant);
if (oldOccupant == null) {
onAvailable(resource);
RosterManager.onContactChanged(account, user);
} else {
boolean changed = false;
if (oldOccupant.getAffiliation() != newOccupant.getAffiliation()) {
changed = true;
onAffiliationChanged(resource, newOccupant.getAffiliation());
}
if (oldOccupant.getRole() != newOccupant.getRole()) {
changed = true;
onRoleChanged(resource, newOccupant.getRole());
}
if (oldOccupant.getStatusMode() != newOccupant.getStatusMode()
|| !oldOccupant.getStatusText().equals(newOccupant.getStatusText())) {
changed = true;
onStatusChanged(resource, newOccupant.getStatusMode(), newOccupant.getStatusText());
}
if (changed) {
RosterManager.onContactChanged(account, user);
}
}
} else if (presence.getType() == Presence.Type.unavailable && state == RoomState.available) {
occupants.remove(resource);
MUCUser mucUser = MUCUser.from(presence);
if (mucUser != null && mucUser.getStatus() != null) {
if (mucUser.getStatus().contains(MUCUser.Status.KICKED_307)) {
onKick(resource, mucUser.getItem().getActor());
} else if (mucUser.getStatus().contains(MUCUser.Status.BANNED_301)){
onBan(resource, mucUser.getItem().getActor());
} else if (mucUser.getStatus().contains(MUCUser.Status.NEW_NICKNAME_303)) {
Resourcepart newNick = mucUser.getItem().getNick();
if (newNick == null) {
return true;
}
onRename(resource, newNick);
Occupant occupant = createOccupant(newNick, presence);
occupants.put(newNick, occupant);
} else if (mucUser.getStatus().contains(MUCUser.Status.REMOVED_AFFIL_CHANGE_321)) {
onRevoke(resource, mucUser.getItem().getActor());
}
} else {
onLeave(resource);
}
RosterManager.onContactChanged(account, user);
}
}
return true;
}
/**
* @return Whether status change action should be added to the chat history.
*/
private boolean showStatusChange() {
return SettingsManager.chatsShowStatusChange() != ChatsShowStatusChange.never;
}
/**
* @return Whether resource is own nickname.
* @param resource
*/
private boolean isSelf(Resourcepart resource) {
return nickname != null && resource != null && nickname.equals(resource);
}
/**
* Informs that the invitee has declined the invitation.
*/
private void onInvitationDeclined(EntityBareJid from, String reason) {
// TODO
}
/**
* A occupant becomes available.
* @param resource
*/
private void onAvailable(Resourcepart resource) {
if (isSelf(resource)) {
setState(RoomState.available);
if (isRequested()) {
if (showStatusChange()) {
createAndSaveNewMessage(resource, Application.getInstance().getString(
R.string.action_join_complete_to, user),
ChatAction.complete, null, true, true, false, false, null);
}
active = true;
setRequested(false);
} else {
if (showStatusChange()) {
newAction(resource, null, ChatAction.complete);
}
}
} else {
if (state == RoomState.available) {
if (showStatusChange()) {
newAction(resource, null, ChatAction.join);
}
}
}
}
/**
* Warning: this method should be placed with packet provider.
*
* @return New occupant based on presence information.
*/
private Occupant createOccupant(Resourcepart resource, Presence presence) {
Occupant occupant = new Occupant(resource);
org.jxmpp.jid.Jid jid = null;
MUCAffiliation affiliation = MUCAffiliation.none;
MUCRole role = MUCRole.none;
StatusMode statusMode = StatusMode.unavailable;
String statusText = null;
MUCUser mucUser = MUCUser.from(presence);
if (mucUser != null) {
MUCItem item = mucUser.getItem();
if (item != null) {
jid = item.getJid();
try {
affiliation = item.getAffiliation();
} catch (NoSuchElementException e) {
}
try {
role = item.getRole();
} catch (NoSuchElementException e) {
}
statusMode = StatusMode.createStatusMode(presence);
statusText = presence.getStatus();
}
}
if (statusText == null) {
statusText = "";
}
occupant.setJid(jid);
occupant.setAffiliation(affiliation);
occupant.setRole(role);
occupant.setStatusMode(statusMode);
occupant.setStatusText(statusText);
return occupant;
}
private void onAffiliationChanged(Resourcepart resource, MUCAffiliation affiliation) {
}
private void onRoleChanged(Resourcepart resource, MUCRole role) {
}
private void onStatusChanged(Resourcepart resource, StatusMode statusMode, String statusText) {
}
/**
* A occupant leaves room.
* @param resource
*/
private void onLeave(Resourcepart resource) {
if (showStatusChange()) {
newAction(resource, null, ChatAction.leave);
}
if (isSelf(resource)) {
setState(RoomState.waiting);
RosterManager.onContactChanged(account, user);
}
}
/**
* A occupant was kicked.
*
* @param resource
* @param actor
*/
private void onKick(Resourcepart resource, org.jxmpp.jid.Jid actor) {
if (showStatusChange()) {
newAction(resource, actor.toString(), ChatAction.kick);
}
if (isSelf(resource)) {
MUCManager.getInstance().leaveRoom(account, getRoom());
}
}
/**
* A occupant was banned.
*
* @param resource
* @param actor
*/
private void onBan(Resourcepart resource, org.jxmpp.jid.Jid actor) {
if (showStatusChange()) {
newAction(resource, actor.toString(), ChatAction.ban);
}
if (isSelf(resource)) {
MUCManager.getInstance().leaveRoom(account, getRoom());
}
}
/**
* A occupant has changed his nickname in the room.
*
* @param resource
* @param newNick
*/
private void onRename(Resourcepart resource, Resourcepart newNick) {
if (showStatusChange()) {
newAction(resource, newNick.toString(), ChatAction.nickname);
}
}
/**
* A user's membership was revoked from the room
*
* @param resource
* @param actor
*/
private void onRevoke(Resourcepart resource, org.jxmpp.jid.Jid actor) {
if (showStatusChange()) {
newAction(resource, actor.toString(), ChatAction.kick);
}
if (isSelf(resource)) {
MUCManager.getInstance().leaveRoom(account, getRoom());
}
}
@Override
protected void onComplete() {
super.onComplete();
if (getState() == RoomState.waiting) {
MUCManager.getInstance().joinRoom(account, getRoom(), false);
}
}
@Override
protected void onDisconnect() {
super.onDisconnect();
if (state != RoomState.unavailable) {
setState(RoomState.waiting);
}
}
}