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