Commit 584973dd authored by Leon Roy's avatar Leon Roy Committed by leonroy

OF-664 - Monitoring archive shows null in room chat logs

git-svn-id: http://svn.igniterealtime.org/svn/repos/openfire/trunk@13627 b35dd754-fafc-0310-a699-88a17e54d16e
parent 7668d387
...@@ -50,31 +50,25 @@ import org.jivesoftware.openfire.user.UserNotFoundException; ...@@ -50,31 +50,25 @@ import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.LocaleUtils; import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.NotFoundException; import org.jivesoftware.util.NotFoundException;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.util.cache.ExternalizableUtil; import org.jivesoftware.util.cache.ExternalizableUtil;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.xmpp.packet.JID; import org.xmpp.packet.JID;
/** /**
* Represents an IM conversation between two people. A conversation encompasses a * Represents an IM conversation between two people. A conversation encompasses a series of messages sent back and forth. It may cover a single topic
* series of messages sent back and forth. It may cover a single topic or several. * or several. The start of a conversation occurs when the first message between two users is sent. It ends when either:
* The start of a conversation occurs when the first message between two users is
* sent. It ends when either:
* <ul> * <ul>
* <li>No messages are sent between the users for a certain period of time (default of 10 * <li>No messages are sent between the users for a certain period of time (default of 10 minutes). The default value can be overridden by setting the
* minutes). The default value can be overridden by setting the Openfire property * Openfire property <tt>conversation.idleTime</tt>.</li>
* <tt>conversation.idleTime</tt>.</li> * <li>The total conversation time reaches a maximum value (default of 60 minutes). The default value can be overridden by setting the Openfire
* <li>The total conversation time reaches a maximum value (default of 60 minutes). * property <tt>conversation.maxTime</tt>. When the max time has been reached and additional messages are sent between the users, a new conversation
* The default value can be overridden by setting the Openfire property * will simply be started.</li>
* <tt>conversation.maxTime</tt>. When the max time has been reached and additional
* messages are sent between the users, a new conversation will simply be
* started.</li>
* </ul> * </ul>
* <p/> * <p/>
* Each conversation has a start time, date of the last message, and count of the * Each conversation has a start time, date of the last message, and count of the messages in the conversation. Conversations are specially marked if
* messages in the conversation. Conversations are specially marked if one of the * one of the participants is on an external server. If archiving is enabled, the actual messages in the conversation can be retrieved.
* participants is on an external server. If archiving is enabled, the actual messages in
* the conversation can be retrieved.
* *
* @author Matt Tucker * @author Matt Tucker
*/ */
...@@ -83,21 +77,16 @@ public class Conversation implements Externalizable { ...@@ -83,21 +77,16 @@ public class Conversation implements Externalizable {
private static final Logger Log = LoggerFactory.getLogger(Conversation.class); private static final Logger Log = LoggerFactory.getLogger(Conversation.class);
private static final String INSERT_CONVERSATION = private static final String INSERT_CONVERSATION = "INSERT INTO ofConversation(conversationID, room, isExternal, startDate, "
"INSERT INTO ofConversation(conversationID, room, isExternal, startDate, " + + "lastActivity, messageCount) VALUES (?,?,?,?,?,0)";
"lastActivity, messageCount) VALUES (?,?,?,?,?,0)"; private static final String INSERT_PARTICIPANT = "INSERT INTO ofConParticipant(conversationID, joinedDate, bareJID, jidResource, nickname) "
private static final String INSERT_PARTICIPANT = + "VALUES (?,?,?,?,?)";
"INSERT INTO ofConParticipant(conversationID, joinedDate, bareJID, jidResource, nickname) " + private static final String LOAD_CONVERSATION = "SELECT room, isExternal, startDate, lastActivity, messageCount "
"VALUES (?,?,?,?,?)"; + "FROM ofConversation WHERE conversationID=?";
private static final String LOAD_CONVERSATION = private static final String LOAD_PARTICIPANTS = "SELECT bareJID, jidResource, nickname, joinedDate, leftDate FROM ofConParticipant "
"SELECT room, isExternal, startDate, lastActivity, messageCount " + + "WHERE conversationID=? ORDER BY joinedDate";
"FROM ofConversation WHERE conversationID=?"; private static final String LOAD_MESSAGES = "SELECT fromJID, fromJIDResource, toJID, toJIDResource, sentDate, body FROM ofMessageArchive WHERE conversationID=? "
private static final String LOAD_PARTICIPANTS = + "ORDER BY sentDate";
"SELECT bareJID, jidResource, nickname, joinedDate, leftDate FROM ofConParticipant " +
"WHERE conversationID=? ORDER BY joinedDate";
private static final String LOAD_MESSAGES =
"SELECT fromJID, toJID, sentDate, body FROM ofMessageArchive WHERE conversationID=? " +
"ORDER BY sentDate";
private transient ConversationManager conversationManager; private transient ConversationManager conversationManager;
...@@ -108,8 +97,7 @@ public class Conversation implements Externalizable { ...@@ -108,8 +97,7 @@ public class Conversation implements Externalizable {
private Date lastActivity; private Date lastActivity;
private int messageCount; private int messageCount;
/** /**
* Room where the group conversion is taking place. For one-to-one chats * Room where the group conversion is taking place. For one-to-one chats there is no room so this variable will be null.
* there is no room so this variable will be null.
*/ */
private JID room; private JID room;
...@@ -122,13 +110,16 @@ public class Conversation implements Externalizable { ...@@ -122,13 +110,16 @@ public class Conversation implements Externalizable {
/** /**
* Constructs a new one-to-one conversation. * Constructs a new one-to-one conversation.
* *
* @param conversationManager the ConversationManager. * @param conversationManager
* @param users the two participants in the conversation. * the ConversationManager.
* @param external true if the conversation includes a user on another server. * @param users
* @param startDate the starting date of the conversation. * the two participants in the conversation.
* @param external
* true if the conversation includes a user on another server.
* @param startDate
* the starting date of the conversation.
*/ */
public Conversation(ConversationManager conversationManager, Collection<JID> users, public Conversation(ConversationManager conversationManager, Collection<JID> users, boolean external, Date startDate) {
boolean external, Date startDate) {
if (users.size() != 2) { if (users.size() != 2) {
throw new IllegalArgumentException("Illegal number of participants: " + users.size()); throw new IllegalArgumentException("Illegal number of participants: " + users.size());
} }
...@@ -147,8 +138,7 @@ public class Conversation implements Externalizable { ...@@ -147,8 +138,7 @@ public class Conversation implements Externalizable {
if (conversationManager.isMetadataArchivingEnabled()) { if (conversationManager.isMetadataArchivingEnabled()) {
try { try {
insertIntoDb(); insertIntoDb();
} } catch (Exception e) {
catch (Exception e) {
Log.error(e.getMessage(), e); Log.error(e.getMessage(), e);
} }
} }
...@@ -157,10 +147,14 @@ public class Conversation implements Externalizable { ...@@ -157,10 +147,14 @@ public class Conversation implements Externalizable {
/** /**
* Constructs a new group chat conversation that is taking place in a room. * Constructs a new group chat conversation that is taking place in a room.
* *
* @param conversationManager the ConversationManager. * @param conversationManager
* @param room the JID of the room where the conversation is taking place. * the ConversationManager.
* @param external true if the conversation includes a user on another server. * @param room
* @param startDate the starting date of the conversation. * the JID of the room where the conversation is taking place.
* @param external
* true if the conversation includes a user on another server.
* @param startDate
* the starting date of the conversation.
*/ */
public Conversation(ConversationManager conversationManager, JID room, boolean external, Date startDate) { public Conversation(ConversationManager conversationManager, JID room, boolean external, Date startDate) {
this.conversationManager = conversationManager; this.conversationManager = conversationManager;
...@@ -182,8 +176,7 @@ public class Conversation implements Externalizable { ...@@ -182,8 +176,7 @@ public class Conversation implements Externalizable {
if (conversationManager.isMetadataArchivingEnabled()) { if (conversationManager.isMetadataArchivingEnabled()) {
try { try {
insertIntoDb(); insertIntoDb();
} } catch (Exception e) {
catch (Exception e) {
Log.error(e.getMessage(), e); Log.error(e.getMessage(), e);
} }
} }
...@@ -192,33 +185,32 @@ public class Conversation implements Externalizable { ...@@ -192,33 +185,32 @@ public class Conversation implements Externalizable {
/** /**
* Loads a conversation from the database. * Loads a conversation from the database.
* *
* @param conversationManager the conversation manager. * @param conversationManager
* @param conversationID the ID of the conversation. * the conversation manager.
* @throws NotFoundException if the conversation can't be loaded. * @param conversationID
* the ID of the conversation.
* @throws NotFoundException
* if the conversation can't be loaded.
*/ */
public Conversation(ConversationManager conversationManager, long conversationID) public Conversation(ConversationManager conversationManager, long conversationID) throws NotFoundException {
throws NotFoundException {
this.conversationManager = conversationManager; this.conversationManager = conversationManager;
this.conversationID = conversationID; this.conversationID = conversationID;
loadFromDb(); loadFromDb();
} }
/** /**
* Returns the unique ID of the conversation. A unique ID is only meaningful when * Returns the unique ID of the conversation. A unique ID is only meaningful when conversation archiving is enabled. Therefore, this method
* conversation archiving is enabled. Therefore, this method returns <tt>-1</tt> if * returns <tt>-1</tt> if archiving is not turned on.
* archiving is not turned on.
* *
* @return the unique ID of the conversation, or <tt>-1</tt> if conversation * @return the unique ID of the conversation, or <tt>-1</tt> if conversation archiving is not enabled.
* archiving is not enabled.
*/ */
public long getConversationID() { public long getConversationID() {
return conversationID; return conversationID;
} }
/** /**
* Returns the JID of the room where the group conversation took place. If the conversation * Returns the JID of the room where the group conversation took place. If the conversation was a one-to-one chat then a <tt>null</tt> value is
* was a one-to-one chat then a <tt>null</tt> value is returned. * returned.
* *
* @return the JID of room or null if this was a one-to-one chat. * @return the JID of room or null if this was a one-to-one chat.
*/ */
...@@ -240,11 +232,11 @@ public class Conversation implements Externalizable { ...@@ -240,11 +232,11 @@ public class Conversation implements Externalizable {
} }
/** /**
* Returns the participations of the specified user (full JID) in this conversation. Each * Returns the participations of the specified user (full JID) in this conversation. Each participation will hold the time when the user joined
* participation will hold the time when the user joined and left the conversation and the * and left the conversation and the nickname if the room happened in a room.
* nickname if the room happened in a room.
* *
* @param user the full JID of the user. * @param user
* the full JID of the user.
* @return the participations of the specified user (full JID) in this conversation. * @return the participations of the specified user (full JID) in this conversation.
*/ */
public Collection<ConversationParticipation> getParticipations(JID user) { public Collection<ConversationParticipation> getParticipations(JID user) {
...@@ -292,18 +284,16 @@ public class Conversation implements Externalizable { ...@@ -292,18 +284,16 @@ public class Conversation implements Externalizable {
} }
/** /**
* Returns the archived messages in the conversation. If message archiving is not * Returns the archived messages in the conversation. If message archiving is not enabled, this method will always return an empty collection.
* enabled, this method will always return an empty collection. This method will only * This method will only return messages that have already been batch-archived to the database; in other words, it does not provide a real-time
* return messages that have already been batch-archived to the database; in other * view of new messages.
* words, it does not provide a real-time view of new messages.
* *
* @return the archived messages in the conversation. * @return the archived messages in the conversation.
*/ */
public List<ArchivedMessage> getMessages() { public List<ArchivedMessage> getMessages() {
if (room == null && !conversationManager.isMessageArchivingEnabled()) { if (room == null && !conversationManager.isMessageArchivingEnabled()) {
return Collections.emptyList(); return Collections.emptyList();
} } else if (room != null && !conversationManager.isRoomArchivingEnabled()) {
else if (room != null && !conversationManager.isRoomArchivingEnabled()) {
return Collections.emptyList(); return Collections.emptyList();
} }
...@@ -318,16 +308,22 @@ public class Conversation implements Externalizable { ...@@ -318,16 +308,22 @@ public class Conversation implements Externalizable {
rs = pstmt.executeQuery(); rs = pstmt.executeQuery();
while (rs.next()) { while (rs.next()) {
JID fromJID = new JID(rs.getString(1)); JID fromJID = new JID(rs.getString(1));
JID toJID = new JID(rs.getString(2)); String fromJIDResource = rs.getString(2);
Date date = new Date(rs.getLong(3)); if (fromJIDResource != null && !"".equals(fromJIDResource)) {
String body = DbConnectionManager.getLargeTextField(rs, 4); fromJID = new JID(rs.getString(1) + "/" + fromJIDResource);
messages.add(new ArchivedMessage(conversationID, fromJID, toJID, date, body, false));
} }
JID toJID = new JID(rs.getString(3));
String toJIDResource = rs.getString(4);
if (toJIDResource != null && !"".equals(toJIDResource)) {
toJID = new JID(rs.getString(1) + "/" + toJIDResource);
} }
catch (SQLException sqle) { Date date = new Date(rs.getLong(5));
Log.error(sqle.getMessage(), sqle); String body = DbConnectionManager.getLargeTextField(rs, 6);
messages.add(new ArchivedMessage(conversationID, fromJID, toJID, date, body, false));
} }
finally { } catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle);
} finally {
DbConnectionManager.closeConnection(rs, pstmt, con); DbConnectionManager.closeConnection(rs, pstmt, con);
} }
// Add messages of users joining or leaving the group chat conversation // Add messages of users joining or leaving the group chat conversation
...@@ -338,8 +334,7 @@ public class Conversation implements Externalizable { ...@@ -338,8 +334,7 @@ public class Conversation implements Externalizable {
String name; String name;
try { try {
name = UserNameManager.getUserName(user); name = UserNameManager.getUserName(user);
} } catch (UserNotFoundException e) {
catch (UserNotFoundException e) {
name = user.toBareJID(); name = user.toBareJID();
anonymous = true; anonymous = true;
} }
...@@ -356,18 +351,15 @@ public class Conversation implements Externalizable { ...@@ -356,18 +351,15 @@ public class Conversation implements Externalizable {
Arrays.asList(participation.getNickname())); Arrays.asList(participation.getNickname()));
leftBody = LocaleUtils.getLocalizedString("muc.conversation.left.anonymous", MonitoringConstants.NAME, leftBody = LocaleUtils.getLocalizedString("muc.conversation.left.anonymous", MonitoringConstants.NAME,
Arrays.asList(participation.getNickname())); Arrays.asList(participation.getNickname()));
} } else {
else {
joinBody = LocaleUtils.getLocalizedString("muc.conversation.joined", MonitoringConstants.NAME, joinBody = LocaleUtils.getLocalizedString("muc.conversation.joined", MonitoringConstants.NAME,
Arrays.asList(participation.getNickname(), name)); Arrays.asList(participation.getNickname(), name));
leftBody = LocaleUtils.getLocalizedString("muc.conversation.left", MonitoringConstants.NAME, leftBody = LocaleUtils.getLocalizedString("muc.conversation.left", MonitoringConstants.NAME,
Arrays.asList(participation.getNickname(), name)); Arrays.asList(participation.getNickname(), name));
} }
messages.add( messages.add(new ArchivedMessage(conversationID, user, jid, participation.getJoined(), joinBody, true));
new ArchivedMessage(conversationID, user, jid, participation.getJoined(), joinBody, true));
if (participation.getLeft() != null) { if (participation.getLeft() != null) {
messages.add(new ArchivedMessage(conversationID, user, jid, participation.getLeft(), leftBody, messages.add(new ArchivedMessage(conversationID, user, jid, participation.getLeft(), leftBody, true));
true));
} }
} }
} }
...@@ -396,12 +388,13 @@ public class Conversation implements Externalizable { ...@@ -396,12 +388,13 @@ public class Conversation implements Externalizable {
} }
/** /**
* Called when a new message for the conversation is received. Each time a new * Called when a new message for the conversation is received. Each time a new message is received, the last activity date will be updated and the
* message is received, the last activity date will be updated and the message * message count incremented.
* count incremented.
* *
* @param entity JID of the entity that sent the message. * @param entity
* @param date the date the message was sent. * JID of the entity that sent the message.
* @param date
* the date the message was sent.
*/ */
synchronized void messageReceived(JID entity, Date date) { synchronized void messageReceived(JID entity, Date date) {
lastActivity = date; lastActivity = date;
...@@ -416,8 +409,7 @@ public class Conversation implements Externalizable { ...@@ -416,8 +409,7 @@ public class Conversation implements Externalizable {
if (userParticipations == null) { if (userParticipations == null) {
userParticipations = new UserParticipations(true); userParticipations = new UserParticipations(true);
participants.put(user.toString(), userParticipations); participants.put(user.toString(), userParticipations);
} } else {
else {
// Get last known participation and check that the user has finished it // Get last known participation and check that the user has finished it
ConversationParticipation lastParticipation = userParticipations.getRecentParticipation(); ConversationParticipation lastParticipation = userParticipations.getRecentParticipation();
if (lastParticipation != null && lastParticipation.getLeft() == null) { if (lastParticipation != null && lastParticipation.getLeft() == null) {
...@@ -436,13 +428,11 @@ public class Conversation implements Externalizable { ...@@ -436,13 +428,11 @@ public class Conversation implements Externalizable {
if (conversationID == -1) { if (conversationID == -1) {
// Save new conversation to the database // Save new conversation to the database
insertIntoDb(); insertIntoDb();
} } else {
else {
// Store new participation information // Store new participation information
insertIntoDb(user, nickname, timestamp); insertIntoDb(user, nickname, timestamp);
} }
} } catch (Exception e) {
catch (Exception e) {
Log.error(e.getMessage(), e); Log.error(e.getMessage(), e);
} }
} }
...@@ -453,14 +443,12 @@ public class Conversation implements Externalizable { ...@@ -453,14 +443,12 @@ public class Conversation implements Externalizable {
UserParticipations userParticipations = participants.get(user.toString()); UserParticipations userParticipations = participants.get(user.toString());
if (userParticipations == null) { if (userParticipations == null) {
Log.warn("Found user that left a conversation but never started it: " + user); Log.warn("Found user that left a conversation but never started it: " + user);
} } else {
else {
// Get last known participation and check that the user has not finished it // Get last known participation and check that the user has not finished it
ConversationParticipation currentParticipation = userParticipations.getRecentParticipation(); ConversationParticipation currentParticipation = userParticipations.getRecentParticipation();
if (currentParticipation == null || currentParticipation.getLeft() != null) { if (currentParticipation == null || currentParticipation.getLeft() != null) {
Log.warn("Found user that left a conversation but never started it: " + user); Log.warn("Found user that left a conversation but never started it: " + user);
} } else {
else {
currentParticipation.participationEnded(new Date(timestamp)); currentParticipation.participationEnded(new Date(timestamp));
// Queue storeage of updated participation information // Queue storeage of updated participation information
conversationManager.queueParticipantLeft(this, user, currentParticipation); conversationManager.queueParticipantLeft(this, user, currentParticipation);
...@@ -471,7 +459,8 @@ public class Conversation implements Externalizable { ...@@ -471,7 +459,8 @@ public class Conversation implements Externalizable {
/** /**
* Inserts a new conversation into the database. * Inserts a new conversation into the database.
* *
* @throws SQLException if an error occurs inserting the conversation. * @throws SQLException
* if an error occurs inserting the conversation.
*/ */
private void insertIntoDb() throws SQLException { private void insertIntoDb() throws SQLException {
this.conversationID = SequenceManager.nextID(this); this.conversationID = SequenceManager.nextID(this);
...@@ -501,12 +490,10 @@ public class Conversation implements Externalizable { ...@@ -501,12 +490,10 @@ public class Conversation implements Externalizable {
} }
} }
pstmt.close(); pstmt.close();
} } catch (SQLException sqle) {
catch (SQLException sqle) {
abortTransaction = true; abortTransaction = true;
throw sqle; throw sqle;
} } finally {
finally {
DbConnectionManager.closeTransactionConnection(con, abortTransaction); DbConnectionManager.closeTransactionConnection(con, abortTransaction);
} }
} }
...@@ -514,10 +501,14 @@ public class Conversation implements Externalizable { ...@@ -514,10 +501,14 @@ public class Conversation implements Externalizable {
/** /**
* Adds a new conversation participant into the database. * Adds a new conversation participant into the database.
* *
* @param participant the full JID of the participant. * @param participant
* @param nickname nickname of the user in the room. * the full JID of the participant.
* @param joined timestamp when user joined the conversation. * @param nickname
* @throws SQLException if an error occurs inserting the conversation. * nickname of the user in the room.
* @param joined
* timestamp when user joined the conversation.
* @throws SQLException
* if an error occurs inserting the conversation.
*/ */
private void insertIntoDb(JID participant, String nickname, long joined) throws SQLException { private void insertIntoDb(JID participant, String nickname, long joined) throws SQLException {
Connection con = null; Connection con = null;
...@@ -531,11 +522,9 @@ public class Conversation implements Externalizable { ...@@ -531,11 +522,9 @@ public class Conversation implements Externalizable {
pstmt.setString(5, nickname); pstmt.setString(5, nickname);
pstmt.executeUpdate(); pstmt.executeUpdate();
pstmt.close(); pstmt.close();
} } catch (SQLException sqle) {
catch (SQLException sqle) {
throw sqle; throw sqle;
} } finally {
finally {
DbConnectionManager.closeConnection(con); DbConnectionManager.closeConnection(con);
} }
} }
...@@ -570,8 +559,7 @@ public class Conversation implements Externalizable { ...@@ -570,8 +559,7 @@ public class Conversation implements Externalizable {
String resource = rs.getString(2); String resource = rs.getString(2);
JID fullJID = new JID("".equals(resource) ? baredJID : baredJID + "/" + resource); JID fullJID = new JID("".equals(resource) ? baredJID : baredJID + "/" + resource);
// Rebuild joined and left time // Rebuild joined and left time
ConversationParticipation participation = ConversationParticipation participation = new ConversationParticipation(new Date(rs.getLong(4)), rs.getString(3));
new ConversationParticipation(new Date(rs.getLong(4)), rs.getString(3));
if (rs.getLong(5) > 0) { if (rs.getLong(5) > 0) {
participation.participationEnded(new Date(rs.getLong(5))); participation.participationEnded(new Date(rs.getLong(5)));
} }
...@@ -583,20 +571,18 @@ public class Conversation implements Externalizable { ...@@ -583,20 +571,18 @@ public class Conversation implements Externalizable {
} }
userParticipations.addParticipation(participation); userParticipations.addParticipation(participation);
} }
} } catch (SQLException sqle) {
catch (SQLException sqle) {
Log.error(sqle.getMessage(), sqle); Log.error(sqle.getMessage(), sqle);
} } finally {
finally {
DbConnectionManager.closeConnection(rs, pstmt, con); DbConnectionManager.closeConnection(rs, pstmt, con);
} }
} }
/** /**
* Notification message inficating that conversation has finished so remaining participants * Notification message inficating that conversation has finished so remaining participants should be marked that they left the conversation.
* should be marked that they left the conversation.
* *
* @param nowDate the date when the conversation was finished * @param nowDate
* the date when the conversation was finished
*/ */
void conversationEnded(Date nowDate) { void conversationEnded(Date nowDate) {
for (Map.Entry<String, UserParticipations> entry : participants.entrySet()) { for (Map.Entry<String, UserParticipations> entry : participants.entrySet()) {
...@@ -623,11 +609,9 @@ public class Conversation implements Externalizable { ...@@ -623,11 +609,9 @@ public class Conversation implements Externalizable {
} }
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin( MonitoringPlugin plugin = (MonitoringPlugin) XMPPServer.getInstance().getPluginManager().getPlugin(MonitoringConstants.NAME);
MonitoringConstants.NAME);
conversationManager = (ConversationManager) plugin.getModule(ConversationManager.class); conversationManager = (ConversationManager) plugin.getModule(ConversationManager.class);
this.participants = new ConcurrentHashMap<String, UserParticipations>(); this.participants = new ConcurrentHashMap<String, UserParticipations>();
conversationID = ExternalizableUtil.getInstance().readLong(in); conversationID = ExternalizableUtil.getInstance().readLong(in);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment