Message.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.ContentValues;
   4import android.database.Cursor;
   5import android.graphics.Color;
   6import android.text.SpannableStringBuilder;
   7import android.util.Log;
   8
   9import org.json.JSONException;
  10
  11import java.lang.ref.WeakReference;
  12import java.net.MalformedURLException;
  13import java.net.URL;
  14import java.util.ArrayList;
  15import java.util.Collections;
  16import java.util.HashSet;
  17import java.util.Iterator;
  18import java.util.List;
  19import java.util.Set;
  20
  21import eu.siacs.conversations.Config;
  22import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
  23import eu.siacs.conversations.services.AvatarService;
  24import eu.siacs.conversations.utils.CryptoHelper;
  25import eu.siacs.conversations.utils.Emoticons;
  26import eu.siacs.conversations.utils.GeoHelper;
  27import eu.siacs.conversations.utils.MessageUtils;
  28import eu.siacs.conversations.utils.MimeUtils;
  29import eu.siacs.conversations.utils.UIHelper;
  30import eu.siacs.conversations.xmpp.Jid;
  31
  32public class Message extends AbstractEntity implements AvatarService.Avatarable  {
  33
  34	public static final String TABLENAME = "messages";
  35
  36	public static final int STATUS_RECEIVED = 0;
  37	public static final int STATUS_UNSEND = 1;
  38	public static final int STATUS_SEND = 2;
  39	public static final int STATUS_SEND_FAILED = 3;
  40	public static final int STATUS_WAITING = 5;
  41	public static final int STATUS_OFFERED = 6;
  42	public static final int STATUS_SEND_RECEIVED = 7;
  43	public static final int STATUS_SEND_DISPLAYED = 8;
  44
  45	public static final int ENCRYPTION_NONE = 0;
  46	public static final int ENCRYPTION_PGP = 1;
  47	public static final int ENCRYPTION_OTR = 2;
  48	public static final int ENCRYPTION_DECRYPTED = 3;
  49	public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
  50	public static final int ENCRYPTION_AXOLOTL = 5;
  51	public static final int ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE = 6;
  52	public static final int ENCRYPTION_AXOLOTL_FAILED = 7;
  53
  54	public static final int TYPE_TEXT = 0;
  55	public static final int TYPE_IMAGE = 1;
  56	public static final int TYPE_FILE = 2;
  57	public static final int TYPE_STATUS = 3;
  58	public static final int TYPE_PRIVATE = 4;
  59	public static final int TYPE_PRIVATE_FILE = 5;
  60	public static final int TYPE_RTP_SESSION = 6;
  61
  62	public static final String CONVERSATION = "conversationUuid";
  63	public static final String COUNTERPART = "counterpart";
  64	public static final String TRUE_COUNTERPART = "trueCounterpart";
  65	public static final String BODY = "body";
  66	public static final String BODY_LANGUAGE = "bodyLanguage";
  67	public static final String TIME_SENT = "timeSent";
  68	public static final String ENCRYPTION = "encryption";
  69	public static final String STATUS = "status";
  70	public static final String TYPE = "type";
  71	public static final String CARBON = "carbon";
  72	public static final String OOB = "oob";
  73	public static final String EDITED = "edited";
  74	public static final String REMOTE_MSG_ID = "remoteMsgId";
  75	public static final String SERVER_MSG_ID = "serverMsgId";
  76	public static final String RELATIVE_FILE_PATH = "relativeFilePath";
  77	public static final String FINGERPRINT = "axolotl_fingerprint";
  78	public static final String READ = "read";
  79	public static final String ERROR_MESSAGE = "errorMsg";
  80	public static final String READ_BY_MARKERS = "readByMarkers";
  81	public static final String MARKABLE = "markable";
  82	public static final String DELETED = "deleted";
  83	public static final String ME_COMMAND = "/me ";
  84
  85	public static final String ERROR_MESSAGE_CANCELLED = "eu.siacs.conversations.cancelled";
  86
  87
  88	public boolean markable = false;
  89	protected String conversationUuid;
  90	protected Jid counterpart;
  91	protected Jid trueCounterpart;
  92	protected String body;
  93	protected String encryptedBody;
  94	protected long timeSent;
  95	protected int encryption;
  96	protected int status;
  97	protected int type;
  98	protected boolean deleted = false;
  99	protected boolean carbon = false;
 100	protected boolean oob = false;
 101	protected List<Edit> edits = new ArrayList<>();
 102	protected String relativeFilePath;
 103	protected boolean read = true;
 104	protected String remoteMsgId = null;
 105	private String bodyLanguage = null;
 106	protected String serverMsgId = null;
 107	private final Conversational conversation;
 108	protected Transferable transferable = null;
 109	private Message mNextMessage = null;
 110	private Message mPreviousMessage = null;
 111	private String axolotlFingerprint = null;
 112	private String errorMessage = null;
 113	private Set<ReadByMarker> readByMarkers = new HashSet<>();
 114
 115	private Boolean isGeoUri = null;
 116	private Boolean isEmojisOnly = null;
 117	private Boolean treatAsDownloadable = null;
 118	private FileParams fileParams = null;
 119	private List<MucOptions.User> counterparts;
 120	private WeakReference<MucOptions.User> user;
 121
 122	protected Message(Conversational conversation) {
 123		this.conversation = conversation;
 124	}
 125
 126	public Message(Conversational conversation, String body, int encryption) {
 127		this(conversation, body, encryption, STATUS_UNSEND);
 128	}
 129
 130	public Message(Conversational conversation, String body, int encryption, int status) {
 131		this(conversation, java.util.UUID.randomUUID().toString(),
 132				conversation.getUuid(),
 133				conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
 134				null,
 135				body,
 136				System.currentTimeMillis(),
 137				encryption,
 138				status,
 139				TYPE_TEXT,
 140				false,
 141				null,
 142				null,
 143				null,
 144				null,
 145				true,
 146				null,
 147				false,
 148				null,
 149				null,
 150				false,
 151				false,
 152				null);
 153	}
 154
 155	public Message(Conversation conversation, int status, int type, final String remoteMsgId) {
 156		this(conversation, java.util.UUID.randomUUID().toString(),
 157				conversation.getUuid(),
 158				conversation.getJid() == null ? null : conversation.getJid().asBareJid(),
 159				null,
 160				null,
 161				System.currentTimeMillis(),
 162				Message.ENCRYPTION_NONE,
 163				status,
 164				type,
 165				false,
 166				remoteMsgId,
 167				null,
 168				null,
 169				null,
 170				true,
 171				null,
 172				false,
 173				null,
 174				null,
 175				false,
 176				false,
 177				null);
 178	}
 179
 180	protected Message(final Conversational conversation, final String uuid, final String conversationUUid, final Jid counterpart,
 181	                final Jid trueCounterpart, final String body, final long timeSent,
 182	                final int encryption, final int status, final int type, final boolean carbon,
 183	                final String remoteMsgId, final String relativeFilePath,
 184	                final String serverMsgId, final String fingerprint, final boolean read,
 185	                final String edited, final boolean oob, final String errorMessage, final Set<ReadByMarker> readByMarkers,
 186	                final boolean markable, final boolean deleted, final String bodyLanguage) {
 187		this.conversation = conversation;
 188		this.uuid = uuid;
 189		this.conversationUuid = conversationUUid;
 190		this.counterpart = counterpart;
 191		this.trueCounterpart = trueCounterpart;
 192		this.body = body == null ? "" : body;
 193		this.timeSent = timeSent;
 194		this.encryption = encryption;
 195		this.status = status;
 196		this.type = type;
 197		this.carbon = carbon;
 198		this.remoteMsgId = remoteMsgId;
 199		this.relativeFilePath = relativeFilePath;
 200		this.serverMsgId = serverMsgId;
 201		this.axolotlFingerprint = fingerprint;
 202		this.read = read;
 203		this.edits = Edit.fromJson(edited);
 204		this.oob = oob;
 205		this.errorMessage = errorMessage;
 206		this.readByMarkers = readByMarkers == null ? new HashSet<>() : readByMarkers;
 207		this.markable = markable;
 208		this.deleted = deleted;
 209		this.bodyLanguage = bodyLanguage;
 210	}
 211
 212	public static Message fromCursor(Cursor cursor, Conversation conversation) {
 213		return new Message(conversation,
 214				cursor.getString(cursor.getColumnIndex(UUID)),
 215				cursor.getString(cursor.getColumnIndex(CONVERSATION)),
 216				fromString(cursor.getString(cursor.getColumnIndex(COUNTERPART))),
 217				fromString(cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART))),
 218				cursor.getString(cursor.getColumnIndex(BODY)),
 219				cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
 220				cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
 221				cursor.getInt(cursor.getColumnIndex(STATUS)),
 222				cursor.getInt(cursor.getColumnIndex(TYPE)),
 223				cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
 224				cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
 225				cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
 226				cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
 227				cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
 228				cursor.getInt(cursor.getColumnIndex(READ)) > 0,
 229				cursor.getString(cursor.getColumnIndex(EDITED)),
 230				cursor.getInt(cursor.getColumnIndex(OOB)) > 0,
 231				cursor.getString(cursor.getColumnIndex(ERROR_MESSAGE)),
 232				ReadByMarker.fromJsonString(cursor.getString(cursor.getColumnIndex(READ_BY_MARKERS))),
 233				cursor.getInt(cursor.getColumnIndex(MARKABLE)) > 0,
 234				cursor.getInt(cursor.getColumnIndex(DELETED)) > 0,
 235				cursor.getString(cursor.getColumnIndex(BODY_LANGUAGE))
 236		);
 237	}
 238
 239	private static Jid fromString(String value) {
 240		try {
 241			if (value != null) {
 242				return Jid.of(value);
 243			}
 244		} catch (IllegalArgumentException e) {
 245			return null;
 246		}
 247		return null;
 248	}
 249
 250	public static Message createStatusMessage(Conversation conversation, String body) {
 251		final Message message = new Message(conversation);
 252		message.setType(Message.TYPE_STATUS);
 253		message.setStatus(Message.STATUS_RECEIVED);
 254		message.body = body;
 255		return message;
 256	}
 257
 258	public static Message createLoadMoreMessage(Conversation conversation) {
 259		final Message message = new Message(conversation);
 260		message.setType(Message.TYPE_STATUS);
 261		message.body = "LOAD_MORE";
 262		return message;
 263	}
 264
 265	@Override
 266	public ContentValues getContentValues() {
 267		ContentValues values = new ContentValues();
 268		values.put(UUID, uuid);
 269		values.put(CONVERSATION, conversationUuid);
 270		if (counterpart == null) {
 271			values.putNull(COUNTERPART);
 272		} else {
 273			values.put(COUNTERPART, counterpart.toString());
 274		}
 275		if (trueCounterpart == null) {
 276			values.putNull(TRUE_COUNTERPART);
 277		} else {
 278			values.put(TRUE_COUNTERPART, trueCounterpart.toString());
 279		}
 280		values.put(BODY, body.length() > Config.MAX_STORAGE_MESSAGE_CHARS ? body.substring(0, Config.MAX_STORAGE_MESSAGE_CHARS) : body);
 281		values.put(TIME_SENT, timeSent);
 282		values.put(ENCRYPTION, encryption);
 283		values.put(STATUS, status);
 284		values.put(TYPE, type);
 285		values.put(CARBON, carbon ? 1 : 0);
 286		values.put(REMOTE_MSG_ID, remoteMsgId);
 287		values.put(RELATIVE_FILE_PATH, relativeFilePath);
 288		values.put(SERVER_MSG_ID, serverMsgId);
 289		values.put(FINGERPRINT, axolotlFingerprint);
 290		values.put(READ, read ? 1 : 0);
 291		try {
 292			values.put(EDITED, Edit.toJson(edits));
 293		} catch (JSONException e) {
 294			Log.e(Config.LOGTAG,"error persisting json for edits",e);
 295		}
 296		values.put(OOB, oob ? 1 : 0);
 297		values.put(ERROR_MESSAGE, errorMessage);
 298		values.put(READ_BY_MARKERS, ReadByMarker.toJson(readByMarkers).toString());
 299		values.put(MARKABLE, markable ? 1 : 0);
 300		values.put(DELETED, deleted ? 1 : 0);
 301		values.put(BODY_LANGUAGE, bodyLanguage);
 302		return values;
 303	}
 304
 305	public String getConversationUuid() {
 306		return conversationUuid;
 307	}
 308
 309	public Conversational getConversation() {
 310		return this.conversation;
 311	}
 312
 313	public Jid getCounterpart() {
 314		return counterpart;
 315	}
 316
 317	public void setCounterpart(final Jid counterpart) {
 318		this.counterpart = counterpart;
 319	}
 320
 321	public Contact getContact() {
 322		if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
 323			return this.conversation.getContact();
 324		} else {
 325			if (this.trueCounterpart == null) {
 326				return null;
 327			} else {
 328				return this.conversation.getAccount().getRoster()
 329						.getContactFromContactList(this.trueCounterpart);
 330			}
 331		}
 332	}
 333
 334	public String getBody() {
 335		return body;
 336	}
 337
 338	public synchronized void setBody(String body) {
 339		if (body == null) {
 340			throw new Error("You should not set the message body to null");
 341		}
 342		this.body = body;
 343		this.isGeoUri = null;
 344		this.isEmojisOnly = null;
 345		this.treatAsDownloadable = null;
 346		this.fileParams = null;
 347	}
 348
 349	public void setMucUser(MucOptions.User user) {
 350		this.user = new WeakReference<>(user);
 351	}
 352
 353	public boolean sameMucUser(Message otherMessage) {
 354		final MucOptions.User thisUser = this.user == null ? null : this.user.get();
 355		final MucOptions.User otherUser = otherMessage.user == null ? null : otherMessage.user.get();
 356		return thisUser != null && thisUser == otherUser;
 357	}
 358
 359	public String getErrorMessage() {
 360		return errorMessage;
 361	}
 362
 363	public boolean setErrorMessage(String message) {
 364		boolean changed = (message != null && !message.equals(errorMessage))
 365				|| (message == null && errorMessage != null);
 366		this.errorMessage = message;
 367		return changed;
 368	}
 369
 370	public long getTimeSent() {
 371		return timeSent;
 372	}
 373
 374	public int getEncryption() {
 375		return encryption;
 376	}
 377
 378	public void setEncryption(int encryption) {
 379		this.encryption = encryption;
 380	}
 381
 382	public int getStatus() {
 383		return status;
 384	}
 385
 386	public void setStatus(int status) {
 387		this.status = status;
 388	}
 389
 390	public String getRelativeFilePath() {
 391		return this.relativeFilePath;
 392	}
 393
 394	public void setRelativeFilePath(String path) {
 395		this.relativeFilePath = path;
 396	}
 397
 398	public String getRemoteMsgId() {
 399		return this.remoteMsgId;
 400	}
 401
 402	public void setRemoteMsgId(String id) {
 403		this.remoteMsgId = id;
 404	}
 405
 406	public String getServerMsgId() {
 407		return this.serverMsgId;
 408	}
 409
 410	public void setServerMsgId(String id) {
 411		this.serverMsgId = id;
 412	}
 413
 414	public boolean isRead() {
 415		return this.read;
 416	}
 417
 418	public boolean isDeleted() {
 419		return this.deleted;
 420	}
 421
 422	public void setDeleted(boolean deleted) {
 423		this.deleted = deleted;
 424	}
 425
 426	public void markRead() {
 427		this.read = true;
 428	}
 429
 430	public void markUnread() {
 431		this.read = false;
 432	}
 433
 434	public void setTime(long time) {
 435		this.timeSent = time;
 436	}
 437
 438	public String getEncryptedBody() {
 439		return this.encryptedBody;
 440	}
 441
 442	public void setEncryptedBody(String body) {
 443		this.encryptedBody = body;
 444	}
 445
 446	public int getType() {
 447		return this.type;
 448	}
 449
 450	public void setType(int type) {
 451		this.type = type;
 452	}
 453
 454	public boolean isCarbon() {
 455		return carbon;
 456	}
 457
 458	public void setCarbon(boolean carbon) {
 459		this.carbon = carbon;
 460	}
 461
 462	public void putEdited(String edited, String serverMsgId) {
 463		final Edit edit = new Edit(edited, serverMsgId);
 464		if (this.edits.size() < 128 && !this.edits.contains(edit)) {
 465			this.edits.add(edit);
 466		}
 467	}
 468
 469	boolean remoteMsgIdMatchInEdit(String id) {
 470		for(Edit edit : this.edits) {
 471			if (id.equals(edit.getEditedId())) {
 472				return true;
 473			}
 474		}
 475		return false;
 476	}
 477
 478	public String getBodyLanguage() {
 479		return this.bodyLanguage;
 480	}
 481
 482	public void setBodyLanguage(String language) {
 483		this.bodyLanguage = language;
 484	}
 485
 486	public boolean edited() {
 487		return this.edits.size() > 0;
 488	}
 489
 490	public void setTrueCounterpart(Jid trueCounterpart) {
 491		this.trueCounterpart = trueCounterpart;
 492	}
 493
 494	public Jid getTrueCounterpart() {
 495		return this.trueCounterpart;
 496	}
 497
 498	public Transferable getTransferable() {
 499		return this.transferable;
 500	}
 501
 502	public synchronized void setTransferable(Transferable transferable) {
 503		this.fileParams = null;
 504		this.transferable = transferable;
 505	}
 506
 507	public boolean addReadByMarker(ReadByMarker readByMarker) {
 508		if (readByMarker.getRealJid() != null) {
 509			if (readByMarker.getRealJid().asBareJid().equals(trueCounterpart)) {
 510				return false;
 511			}
 512		} else if (readByMarker.getFullJid() != null) {
 513			if (readByMarker.getFullJid().equals(counterpart)) {
 514				return false;
 515			}
 516		}
 517		if (this.readByMarkers.add(readByMarker)) {
 518			if (readByMarker.getRealJid() != null && readByMarker.getFullJid() != null) {
 519				Iterator<ReadByMarker> iterator = this.readByMarkers.iterator();
 520				while (iterator.hasNext()) {
 521					ReadByMarker marker = iterator.next();
 522					if (marker.getRealJid() == null && readByMarker.getFullJid().equals(marker.getFullJid())) {
 523						iterator.remove();
 524					}
 525				}
 526			}
 527			return true;
 528		} else {
 529			return false;
 530		}
 531	}
 532
 533	public Set<ReadByMarker> getReadByMarkers() {
 534		return Collections.unmodifiableSet(this.readByMarkers);
 535	}
 536
 537	boolean similar(Message message) {
 538		if (!isPrivateMessage() && this.serverMsgId != null && message.getServerMsgId() != null) {
 539			return this.serverMsgId.equals(message.getServerMsgId()) || Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId());
 540		} else if (Edit.wasPreviouslyEditedServerMsgId(edits, message.getServerMsgId())) {
 541			return true;
 542		} else if (this.body == null || this.counterpart == null) {
 543			return false;
 544		} else {
 545			String body, otherBody;
 546			if (this.hasFileOnRemoteHost()) {
 547				body = getFileParams().url.toString();
 548				otherBody = message.body == null ? null : message.body.trim();
 549			} else {
 550				body = this.body;
 551				otherBody = message.body;
 552			}
 553			final boolean matchingCounterpart = this.counterpart.equals(message.getCounterpart());
 554			if (message.getRemoteMsgId() != null) {
 555				final boolean hasUuid = CryptoHelper.UUID_PATTERN.matcher(message.getRemoteMsgId()).matches();
 556				if (hasUuid && matchingCounterpart && Edit.wasPreviouslyEditedRemoteMsgId(edits, message.getRemoteMsgId())) {
 557					return true;
 558				}
 559				return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
 560						&& matchingCounterpart
 561						&& (body.equals(otherBody) || (message.getEncryption() == Message.ENCRYPTION_PGP && hasUuid));
 562			} else {
 563				return this.remoteMsgId == null
 564						&& matchingCounterpart
 565						&& body.equals(otherBody)
 566						&& Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
 567			}
 568		}
 569	}
 570
 571	public Message next() {
 572		if (this.conversation instanceof Conversation) {
 573			final Conversation conversation = (Conversation) this.conversation;
 574			synchronized (conversation.messages) {
 575				if (this.mNextMessage == null) {
 576					int index = conversation.messages.indexOf(this);
 577					if (index < 0 || index >= conversation.messages.size() - 1) {
 578						this.mNextMessage = null;
 579					} else {
 580						this.mNextMessage = conversation.messages.get(index + 1);
 581					}
 582				}
 583				return this.mNextMessage;
 584			}
 585		} else {
 586			throw new AssertionError("Calling next should be disabled for stubs");
 587		}
 588	}
 589
 590	public Message prev() {
 591		if (this.conversation instanceof Conversation) {
 592			final Conversation conversation = (Conversation) this.conversation;
 593			synchronized (conversation.messages) {
 594				if (this.mPreviousMessage == null) {
 595					int index = conversation.messages.indexOf(this);
 596					if (index <= 0 || index > conversation.messages.size()) {
 597						this.mPreviousMessage = null;
 598					} else {
 599						this.mPreviousMessage = conversation.messages.get(index - 1);
 600					}
 601				}
 602			}
 603			return this.mPreviousMessage;
 604		} else {
 605			throw new AssertionError("Calling prev should be disabled for stubs");
 606		}
 607	}
 608
 609	public boolean isLastCorrectableMessage() {
 610		Message next = next();
 611		while (next != null) {
 612			if (next.isCorrectable()) {
 613				return false;
 614			}
 615			next = next.next();
 616		}
 617		return isCorrectable();
 618	}
 619
 620	private boolean isCorrectable() {
 621		return getStatus() != STATUS_RECEIVED && !isCarbon();
 622	}
 623
 624	public boolean mergeable(final Message message) {
 625		return message != null &&
 626				(message.getType() == Message.TYPE_TEXT &&
 627						this.getTransferable() == null &&
 628						message.getTransferable() == null &&
 629						message.getEncryption() != Message.ENCRYPTION_PGP &&
 630						message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
 631						this.getType() == message.getType() &&
 632						//this.getStatus() == message.getStatus() &&
 633						isStatusMergeable(this.getStatus(), message.getStatus()) &&
 634						this.getEncryption() == message.getEncryption() &&
 635						this.getCounterpart() != null &&
 636						this.getCounterpart().equals(message.getCounterpart()) &&
 637						this.edited() == message.edited() &&
 638						(message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
 639						this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
 640						!message.isGeoUri() &&
 641						!this.isGeoUri() &&
 642						!message.isOOb() &&
 643						!this.isOOb() &&
 644						!message.treatAsDownloadable() &&
 645						!this.treatAsDownloadable() &&
 646						!message.hasMeCommand() &&
 647						!this.hasMeCommand() &&
 648						!this.bodyIsOnlyEmojis() &&
 649						!message.bodyIsOnlyEmojis() &&
 650						((this.axolotlFingerprint == null && message.axolotlFingerprint == null) || this.axolotlFingerprint.equals(message.getFingerprint())) &&
 651						UIHelper.sameDay(message.getTimeSent(), this.getTimeSent()) &&
 652						this.getReadByMarkers().equals(message.getReadByMarkers()) &&
 653						!this.conversation.getJid().asBareJid().equals(Config.BUG_REPORTS)
 654				);
 655	}
 656
 657	private static boolean isStatusMergeable(int a, int b) {
 658		return a == b || (
 659				(a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
 660						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
 661						|| (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_WAITING)
 662						|| (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
 663						|| (a == Message.STATUS_SEND && b == Message.STATUS_WAITING)
 664		);
 665	}
 666
 667	public void setCounterparts(List<MucOptions.User> counterparts) {
 668		this.counterparts = counterparts;
 669	}
 670
 671	public List<MucOptions.User> getCounterparts() {
 672		return this.counterparts;
 673	}
 674
 675	@Override
 676	public int getAvatarBackgroundColor() {
 677		if (type == Message.TYPE_STATUS && getCounterparts() != null && getCounterparts().size() > 1) {
 678			return Color.TRANSPARENT;
 679		} else {
 680			return UIHelper.getColorForName(UIHelper.getMessageDisplayName(this));
 681		}
 682	}
 683
 684	public boolean isOOb() {
 685		return oob;
 686	}
 687
 688	public static class MergeSeparator {
 689	}
 690
 691	public SpannableStringBuilder getMergedBody() {
 692		SpannableStringBuilder body = new SpannableStringBuilder(MessageUtils.filterLtrRtl(this.body).trim());
 693		Message current = this;
 694		while (current.mergeable(current.next())) {
 695			current = current.next();
 696			if (current == null) {
 697				break;
 698			}
 699			body.append("\n\n");
 700			body.setSpan(new MergeSeparator(), body.length() - 2, body.length(),
 701					SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE);
 702			body.append(MessageUtils.filterLtrRtl(current.getBody()).trim());
 703		}
 704		return body;
 705	}
 706
 707	public boolean hasMeCommand() {
 708		return this.body.trim().startsWith(ME_COMMAND);
 709	}
 710
 711	public int getMergedStatus() {
 712		int status = this.status;
 713		Message current = this;
 714		while (current.mergeable(current.next())) {
 715			current = current.next();
 716			if (current == null) {
 717				break;
 718			}
 719			status = current.status;
 720		}
 721		return status;
 722	}
 723
 724	public long getMergedTimeSent() {
 725		long time = this.timeSent;
 726		Message current = this;
 727		while (current.mergeable(current.next())) {
 728			current = current.next();
 729			if (current == null) {
 730				break;
 731			}
 732			time = current.timeSent;
 733		}
 734		return time;
 735	}
 736
 737	public boolean wasMergedIntoPrevious() {
 738		Message prev = this.prev();
 739		return prev != null && prev.mergeable(this);
 740	}
 741
 742	public boolean trusted() {
 743		Contact contact = this.getContact();
 744		return status > STATUS_RECEIVED || (contact != null && (contact.showInContactList() || contact.isSelf()));
 745	}
 746
 747	public boolean fixCounterpart() {
 748		Presences presences = conversation.getContact().getPresences();
 749		if (counterpart != null && presences.has(counterpart.getResource())) {
 750			return true;
 751		} else if (presences.size() >= 1) {
 752			try {
 753				counterpart = Jid.of(conversation.getJid().getLocal(),
 754						conversation.getJid().getDomain(),
 755						presences.toResourceArray()[0]);
 756				return true;
 757			} catch (IllegalArgumentException e) {
 758				counterpart = null;
 759				return false;
 760			}
 761		} else {
 762			counterpart = null;
 763			return false;
 764		}
 765	}
 766
 767	public void setUuid(String uuid) {
 768		this.uuid = uuid;
 769	}
 770
 771	public String getEditedId() {
 772		if (edits.size() > 0) {
 773			return edits.get(edits.size() - 1).getEditedId();
 774		} else {
 775			throw new IllegalStateException("Attempting to store unedited message");
 776		}
 777	}
 778
 779	public String getEditedIdWireFormat() {
 780		if (edits.size() > 0) {
 781			return edits.get(Config.USE_LMC_VERSION_1_1 ? 0 : edits.size() - 1).getEditedId();
 782		} else {
 783			throw new IllegalStateException("Attempting to store unedited message");
 784		}
 785	}
 786
 787	public void setOob(boolean isOob) {
 788		this.oob = isOob;
 789	}
 790
 791	public String getMimeType() {
 792		String extension;
 793		if (relativeFilePath != null) {
 794			extension = MimeUtils.extractRelevantExtension(relativeFilePath);
 795		} else {
 796			try {
 797				final URL url = new URL(body.split("\n")[0]);
 798				extension = MimeUtils.extractRelevantExtension(url);
 799			} catch (MalformedURLException e) {
 800				return null;
 801			}
 802		}
 803		return MimeUtils.guessMimeTypeFromExtension(extension);
 804	}
 805
 806	public synchronized boolean treatAsDownloadable() {
 807		if (treatAsDownloadable == null) {
 808			treatAsDownloadable = MessageUtils.treatAsDownloadable(this.body, this.oob);
 809		}
 810		return treatAsDownloadable;
 811	}
 812
 813	public synchronized boolean bodyIsOnlyEmojis() {
 814		if (isEmojisOnly == null) {
 815			isEmojisOnly = Emoticons.isOnlyEmoji(body.replaceAll("\\s", ""));
 816		}
 817		return isEmojisOnly;
 818	}
 819
 820	public synchronized boolean isGeoUri() {
 821		if (isGeoUri == null) {
 822			isGeoUri = GeoHelper.GEO_URI.matcher(body).matches();
 823		}
 824		return isGeoUri;
 825	}
 826
 827	public synchronized void resetFileParams() {
 828		this.fileParams = null;
 829	}
 830
 831	public synchronized FileParams getFileParams() {
 832		if (fileParams == null) {
 833			fileParams = new FileParams();
 834			if (this.transferable != null) {
 835				fileParams.size = this.transferable.getFileSize();
 836			}
 837			final String[] parts = body == null ? new String[0] : body.split("\\|");
 838			switch (parts.length) {
 839				case 1:
 840					try {
 841						fileParams.size = Long.parseLong(parts[0]);
 842					} catch (NumberFormatException e) {
 843						fileParams.url = parseUrl(parts[0]);
 844					}
 845					break;
 846				case 5:
 847					fileParams.runtime = parseInt(parts[4]);
 848				case 4:
 849					fileParams.width = parseInt(parts[2]);
 850					fileParams.height = parseInt(parts[3]);
 851				case 2:
 852					fileParams.url = parseUrl(parts[0]);
 853					fileParams.size = parseLong(parts[1]);
 854					break;
 855				case 3:
 856					fileParams.size = parseLong(parts[0]);
 857					fileParams.width = parseInt(parts[1]);
 858					fileParams.height = parseInt(parts[2]);
 859					break;
 860			}
 861		}
 862		return fileParams;
 863	}
 864
 865	private static long parseLong(String value) {
 866		try {
 867			return Long.parseLong(value);
 868		} catch (NumberFormatException e) {
 869			return 0;
 870		}
 871	}
 872
 873	private static int parseInt(String value) {
 874		try {
 875			return Integer.parseInt(value);
 876		} catch (NumberFormatException e) {
 877			return 0;
 878		}
 879	}
 880
 881	private static URL parseUrl(String value) {
 882		try {
 883			return new URL(value);
 884		} catch (MalformedURLException e) {
 885			return null;
 886		}
 887	}
 888
 889	public void untie() {
 890		this.mNextMessage = null;
 891		this.mPreviousMessage = null;
 892	}
 893
 894	public boolean isPrivateMessage() {
 895		return type == TYPE_PRIVATE || type == TYPE_PRIVATE_FILE;
 896	}
 897
 898	public boolean isFileOrImage() {
 899		return type == TYPE_FILE || type == TYPE_IMAGE || type == TYPE_PRIVATE_FILE;
 900	}
 901
 902	public boolean hasFileOnRemoteHost() {
 903		return isFileOrImage() && getFileParams().url != null;
 904	}
 905
 906	public boolean needsUploading() {
 907		return isFileOrImage() && getFileParams().url == null;
 908	}
 909
 910	public class FileParams {
 911		public URL url;
 912		public long size = 0;
 913		public int width = 0;
 914		public int height = 0;
 915		public int runtime = 0;
 916	}
 917
 918	public void setFingerprint(String fingerprint) {
 919		this.axolotlFingerprint = fingerprint;
 920	}
 921
 922	public String getFingerprint() {
 923		return axolotlFingerprint;
 924	}
 925
 926	public boolean isTrusted() {
 927		FingerprintStatus s = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
 928		return s != null && s.isTrusted();
 929	}
 930
 931	private int getPreviousEncryption() {
 932		for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()) {
 933			if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
 934				continue;
 935			}
 936			return iterator.getEncryption();
 937		}
 938		return ENCRYPTION_NONE;
 939	}
 940
 941	private int getNextEncryption() {
 942		if (this.conversation instanceof Conversation) {
 943			Conversation conversation = (Conversation) this.conversation;
 944			for (Message iterator = this.next(); iterator != null; iterator = iterator.next()) {
 945				if (iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED) {
 946					continue;
 947				}
 948				return iterator.getEncryption();
 949			}
 950			return conversation.getNextEncryption();
 951		} else {
 952			throw new AssertionError("This should never be called since isInValidSession should be disabled for stubs");
 953		}
 954	}
 955
 956	public boolean isValidInSession() {
 957		int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
 958		int futureEncryption = getCleanedEncryption(this.getNextEncryption());
 959
 960		boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
 961				|| futureEncryption == ENCRYPTION_NONE
 962				|| pastEncryption != futureEncryption;
 963
 964		return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
 965	}
 966
 967	private static int getCleanedEncryption(int encryption) {
 968		if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
 969			return ENCRYPTION_PGP;
 970		}
 971		if (encryption == ENCRYPTION_AXOLOTL_NOT_FOR_THIS_DEVICE || encryption == ENCRYPTION_AXOLOTL_FAILED) {
 972			return ENCRYPTION_AXOLOTL;
 973		}
 974		return encryption;
 975	}
 976
 977	public static boolean configurePrivateMessage(final Message message) {
 978		return configurePrivateMessage(message, false);
 979	}
 980
 981	public static boolean configurePrivateFileMessage(final Message message) {
 982		return configurePrivateMessage(message, true);
 983	}
 984
 985	private static boolean configurePrivateMessage(final Message message, final boolean isFile) {
 986		final Conversation conversation;
 987		if (message.conversation instanceof Conversation) {
 988			conversation = (Conversation) message.conversation;
 989		} else {
 990			return false;
 991		}
 992		if (conversation.getMode() == Conversation.MODE_MULTI) {
 993			final Jid nextCounterpart = conversation.getNextCounterpart();
 994			if (nextCounterpart != null) {
 995				message.setCounterpart(nextCounterpart);
 996				message.setTrueCounterpart(conversation.getMucOptions().getTrueCounterpart(nextCounterpart));
 997				message.setType(isFile ? Message.TYPE_PRIVATE_FILE : Message.TYPE_PRIVATE);
 998				return true;
 999			}
1000		}
1001		return false;
1002	}
1003}