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