Conversation.java

   1package eu.siacs.conversations.entities;
   2
   3import android.content.ContentValues;
   4import android.database.Cursor;
   5import android.support.annotation.NonNull;
   6
   7import net.java.otr4j.OtrException;
   8import net.java.otr4j.crypto.OtrCryptoException;
   9import net.java.otr4j.session.SessionID;
  10import net.java.otr4j.session.SessionImpl;
  11import net.java.otr4j.session.SessionStatus;
  12
  13import org.json.JSONArray;
  14import org.json.JSONException;
  15import org.json.JSONObject;
  16
  17import java.security.interfaces.DSAPublicKey;
  18import java.util.ArrayList;
  19import java.util.Collections;
  20import java.util.Comparator;
  21import java.util.Iterator;
  22import java.util.List;
  23import java.util.ListIterator;
  24import java.util.Locale;
  25import java.util.concurrent.atomic.AtomicBoolean;
  26
  27import eu.siacs.conversations.Config;
  28import eu.siacs.conversations.crypto.PgpDecryptionService;
  29import eu.siacs.conversations.crypto.axolotl.AxolotlService;
  30import eu.siacs.conversations.xmpp.chatstate.ChatState;
  31import eu.siacs.conversations.xmpp.jid.InvalidJidException;
  32import eu.siacs.conversations.xmpp.jid.Jid;
  33
  34
  35public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation> {
  36	public static final String TABLENAME = "conversations";
  37
  38	public static final int STATUS_AVAILABLE = 0;
  39	public static final int STATUS_ARCHIVED = 1;
  40
  41	public static final int MODE_MULTI = 1;
  42	public static final int MODE_SINGLE = 0;
  43
  44	public static final String NAME = "name";
  45	public static final String ACCOUNT = "accountUuid";
  46	public static final String CONTACT = "contactUuid";
  47	public static final String CONTACTJID = "contactJid";
  48	public static final String STATUS = "status";
  49	public static final String CREATED = "created";
  50	public static final String MODE = "mode";
  51	public static final String ATTRIBUTES = "attributes";
  52
  53	public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
  54	public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
  55	public static final String ATTRIBUTE_LAST_CLEAR_HISTORY = "last_clear_history";
  56
  57	private static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
  58
  59	private static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
  60	static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
  61
  62	private String draftMessage;
  63	private String name;
  64	private String contactUuid;
  65	private String accountUuid;
  66	private Jid contactJid;
  67	private int status;
  68	private long created;
  69	private int mode;
  70
  71	private JSONObject attributes = new JSONObject();
  72
  73	private Jid nextCounterpart;
  74
  75	protected final ArrayList<Message> messages = new ArrayList<>();
  76	protected Account account = null;
  77
  78	private transient SessionImpl otrSession;
  79
  80	private transient String otrFingerprint = null;
  81	private Smp mSmp = new Smp();
  82
  83	private String nextMessage;
  84
  85	private transient MucOptions mucOptions = null;
  86
  87	private byte[] symmetricKey;
  88
  89	private Bookmark bookmark;
  90
  91	private boolean messagesLeftOnServer = true;
  92	private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
  93	private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
  94	private String mLastReceivedOtrMessageId = null;
  95	private String mFirstMamReference = null;
  96	private Message correctingMessage;
  97	public AtomicBoolean messagesLoaded = new AtomicBoolean(true);
  98
  99	public boolean hasMessagesLeftOnServer() {
 100		return messagesLeftOnServer;
 101	}
 102
 103	public void setHasMessagesLeftOnServer(boolean value) {
 104		this.messagesLeftOnServer = value;
 105	}
 106
 107
 108	public Message getFirstUnreadMessage() {
 109		Message first = null;
 110		synchronized (this.messages) {
 111			for (int i = messages.size() - 1; i >= 0; --i) {
 112				if (messages.get(i).isRead()) {
 113					return first;
 114				} else {
 115					first = messages.get(i);
 116				}
 117			}
 118		}
 119		return first;
 120	}
 121
 122	public Message findUnsentMessageWithUuid(String uuid) {
 123		synchronized(this.messages) {
 124			for (final Message message : this.messages) {
 125				final int s = message.getStatus();
 126				if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
 127					return message;
 128				}
 129			}
 130		}
 131		return null;
 132	}
 133
 134	public void findWaitingMessages(OnMessageFound onMessageFound) {
 135		synchronized (this.messages) {
 136			for(Message message : this.messages) {
 137				if (message.getStatus() == Message.STATUS_WAITING) {
 138					onMessageFound.onMessageFound(message);
 139				}
 140			}
 141		}
 142	}
 143
 144	public void findUnreadMessages(OnMessageFound onMessageFound) {
 145		synchronized (this.messages) {
 146			for(Message message : this.messages) {
 147				if (!message.isRead()) {
 148					onMessageFound.onMessageFound(message);
 149				}
 150			}
 151		}
 152	}
 153
 154	public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
 155		synchronized (this.messages) {
 156			for (final Message message : this.messages) {
 157				if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
 158						&& message.getEncryption() != Message.ENCRYPTION_PGP) {
 159					onMessageFound.onMessageFound(message);
 160						}
 161			}
 162		}
 163	}
 164
 165	public Message findMessageWithFileAndUuid(final String uuid) {
 166		synchronized (this.messages) {
 167			for (final Message message : this.messages) {
 168				if (message.getUuid().equals(uuid)
 169						&& message.getEncryption() != Message.ENCRYPTION_PGP
 170						&& (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.treatAsDownloadable())) {
 171					return message;
 172				}
 173			}
 174		}
 175		return null;
 176	}
 177
 178	public void clearMessages() {
 179		synchronized (this.messages) {
 180			this.messages.clear();
 181		}
 182	}
 183
 184	public boolean setIncomingChatState(ChatState state) {
 185		if (this.mIncomingChatState == state) {
 186			return false;
 187		}
 188		this.mIncomingChatState = state;
 189		return true;
 190	}
 191
 192	public ChatState getIncomingChatState() {
 193		return this.mIncomingChatState;
 194	}
 195
 196	public boolean setOutgoingChatState(ChatState state) {
 197		if (mode == MODE_MULTI && (getNextCounterpart() != null || !isPnNA())) {
 198			return false;
 199		}
 200		if (this.mOutgoingChatState != state) {
 201			this.mOutgoingChatState = state;
 202			return true;
 203		} else {
 204			return false;
 205		}
 206	}
 207
 208	public ChatState getOutgoingChatState() {
 209		return this.mOutgoingChatState;
 210	}
 211
 212	public void trim() {
 213		synchronized (this.messages) {
 214			final int size = messages.size();
 215			final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
 216			if (size > maxsize) {
 217				List<Message> discards = this.messages.subList(0, size - maxsize);
 218				final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
 219				if (pgpDecryptionService != null) {
 220					pgpDecryptionService.discard(discards);
 221				}
 222				discards.clear();
 223				untieMessages();
 224			}
 225		}
 226	}
 227
 228	public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
 229		synchronized (this.messages) {
 230			for (Message message : this.messages) {
 231				if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
 232						&& (message.getEncryption() == encryptionType)) {
 233					onMessageFound.onMessageFound(message);
 234				}
 235			}
 236		}
 237	}
 238
 239	public void findUnsentTextMessages(OnMessageFound onMessageFound) {
 240		synchronized (this.messages) {
 241			for (Message message : this.messages) {
 242				if (message.getType() != Message.TYPE_IMAGE
 243						&& message.getStatus() == Message.STATUS_UNSEND) {
 244					onMessageFound.onMessageFound(message);
 245						}
 246			}
 247		}
 248	}
 249
 250	public Message findSentMessageWithUuidOrRemoteId(String id) {
 251		synchronized (this.messages) {
 252			for (Message message : this.messages) {
 253				if (id.equals(message.getUuid())
 254						|| (message.getStatus() >= Message.STATUS_SEND
 255						&& id.equals(message.getRemoteMsgId()))) {
 256					return message;
 257				}
 258			}
 259		}
 260		return null;
 261	}
 262
 263	public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
 264		synchronized (this.messages) {
 265			for(int i = this.messages.size() - 1; i >= 0; --i) {
 266				Message message = messages.get(i);
 267				if (counterpart.equals(message.getCounterpart())
 268						&& ((message.getStatus() == Message.STATUS_RECEIVED) == received)
 269						&& (carbon == message.isCarbon() || received) ) {
 270					if (id.equals(message.getRemoteMsgId())) {
 271						return message;
 272					} else {
 273						return null;
 274					}
 275				}
 276			}
 277		}
 278		return null;
 279	}
 280
 281	public Message findSentMessageWithUuid(String id) {
 282		synchronized (this.messages) {
 283			for (Message message : this.messages) {
 284				if (id.equals(message.getUuid())) {
 285					return message;
 286				}
 287			}
 288		}
 289		return null;
 290	}
 291
 292	public boolean hasMessageWithCounterpart(Jid counterpart) {
 293		synchronized (this.messages) {
 294			for(Message message : this.messages) {
 295				if (counterpart.equals(message.getCounterpart())) {
 296					return true;
 297				}
 298			}
 299		}
 300		return false;
 301	}
 302
 303	public void populateWithMessages(final List<Message> messages) {
 304		synchronized (this.messages) {
 305			messages.clear();
 306			messages.addAll(this.messages);
 307		}
 308		for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
 309			if (iterator.next().wasMergedIntoPrevious()) {
 310				iterator.remove();
 311			}
 312		}
 313	}
 314
 315	@Override
 316	public boolean isBlocked() {
 317		return getContact().isBlocked();
 318	}
 319
 320	@Override
 321	public boolean isDomainBlocked() {
 322		return getContact().isDomainBlocked();
 323	}
 324
 325	@Override
 326	public Jid getBlockedJid() {
 327		return getContact().getBlockedJid();
 328	}
 329
 330	public String getLastReceivedOtrMessageId() {
 331		return this.mLastReceivedOtrMessageId;
 332	}
 333
 334	public void setLastReceivedOtrMessageId(String id) {
 335		this.mLastReceivedOtrMessageId = id;
 336	}
 337
 338	public int countMessages() {
 339		synchronized (this.messages) {
 340			return this.messages.size();
 341		}
 342	}
 343
 344	public void setFirstMamReference(String reference) {
 345		this.mFirstMamReference = reference;
 346	}
 347
 348	public String getFirstMamReference() {
 349		return this.mFirstMamReference;
 350	}
 351
 352	public void setLastClearHistory(long time) {
 353		setAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY,String.valueOf(time));
 354	}
 355
 356	public long getLastClearHistory() {
 357		return getLongAttribute(ATTRIBUTE_LAST_CLEAR_HISTORY, 0);
 358	}
 359
 360	public List<Jid> getAcceptedCryptoTargets() {
 361		if (mode == MODE_SINGLE) {
 362			return Collections.singletonList(getJid().toBareJid());
 363		} else {
 364			return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
 365		}
 366	}
 367
 368	public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
 369		setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
 370	}
 371
 372	public boolean setCorrectingMessage(Message correctingMessage) {
 373		this.correctingMessage = correctingMessage;
 374		return correctingMessage == null && draftMessage != null;
 375	}
 376
 377	public Message getCorrectingMessage() {
 378		return this.correctingMessage;
 379	}
 380
 381	public boolean withSelf() {
 382		return getContact().isSelf();
 383	}
 384
 385	@Override
 386	public int compareTo(@NonNull Conversation another) {
 387		final Message left = getLatestMessage();
 388		final Message right = another.getLatestMessage();
 389		if (left.getTimeSent() > right.getTimeSent()) {
 390			return -1;
 391		} else if (left.getTimeSent() < right.getTimeSent()) {
 392			return 1;
 393		} else {
 394			return 0;
 395		}
 396	}
 397
 398	public void setDraftMessage(String draftMessage) {
 399		this.draftMessage = draftMessage;
 400	}
 401
 402	public String getDraftMessage() {
 403		return draftMessage;
 404	}
 405
 406	public interface OnMessageFound {
 407		void onMessageFound(final Message message);
 408	}
 409
 410	public Conversation(final String name, final Account account, final Jid contactJid,
 411			final int mode) {
 412		this(java.util.UUID.randomUUID().toString(), name, null, account
 413				.getUuid(), contactJid, System.currentTimeMillis(),
 414				STATUS_AVAILABLE, mode, "");
 415		this.account = account;
 416	}
 417
 418	public Conversation(final String uuid, final String name, final String contactUuid,
 419			final String accountUuid, final Jid contactJid, final long created, final int status,
 420			final int mode, final String attributes) {
 421		this.uuid = uuid;
 422		this.name = name;
 423		this.contactUuid = contactUuid;
 424		this.accountUuid = accountUuid;
 425		this.contactJid = contactJid;
 426		this.created = created;
 427		this.status = status;
 428		this.mode = mode;
 429		try {
 430			this.attributes = new JSONObject(attributes == null ? "" : attributes);
 431		} catch (JSONException e) {
 432			this.attributes = new JSONObject();
 433		}
 434	}
 435
 436	public boolean isRead() {
 437		return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
 438	}
 439
 440	public List<Message> markRead() {
 441		final List<Message> unread = new ArrayList<>();
 442		synchronized (this.messages) {
 443			for(Message message : this.messages) {
 444				if (!message.isRead()) {
 445					message.markRead();
 446					unread.add(message);
 447				}
 448			}
 449		}
 450		return unread;
 451	}
 452
 453	public Message getLatestMarkableMessage() {
 454		for (int i = this.messages.size() - 1; i >= 0; --i) {
 455			if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
 456					&& this.messages.get(i).markable) {
 457				if (this.messages.get(i).isRead()) {
 458					return null;
 459				} else {
 460					return this.messages.get(i);
 461				}
 462					}
 463		}
 464		return null;
 465	}
 466
 467	public Message getLatestMessage() {
 468		if (this.messages.size() == 0) {
 469			Message message = new Message(this, "", Message.ENCRYPTION_NONE);
 470			message.setType(Message.TYPE_STATUS);
 471			message.setTime(Math.max(getCreated(),getLastClearHistory()));
 472			return message;
 473		} else {
 474			Message message = this.messages.get(this.messages.size() - 1);
 475			message.setConversation(this);
 476			return message;
 477		}
 478	}
 479
 480	public String getName() {
 481		if (getMode() == MODE_MULTI) {
 482			if (getMucOptions().getSubject() != null) {
 483				return getMucOptions().getSubject();
 484			} else if (bookmark != null
 485					&& bookmark.getBookmarkName() != null
 486					&& !bookmark.getBookmarkName().trim().isEmpty()) {
 487				return bookmark.getBookmarkName().trim();
 488			} else {
 489				String generatedName = getMucOptions().createNameFromParticipants();
 490				if (generatedName != null) {
 491					return generatedName;
 492				} else {
 493					return getJid().getUnescapedLocalpart();
 494				}
 495			}
 496		} else if (isWithStranger()) {
 497			return contactJid.toBareJid().toString();
 498		} else {
 499			return this.getContact().getDisplayName();
 500		}
 501	}
 502
 503	public String getAccountUuid() {
 504		return this.accountUuid;
 505	}
 506
 507	public Account getAccount() {
 508		return this.account;
 509	}
 510
 511	public Contact getContact() {
 512		return this.account.getRoster().getContact(this.contactJid);
 513	}
 514
 515	public void setAccount(final Account account) {
 516		this.account = account;
 517	}
 518
 519	@Override
 520	public Jid getJid() {
 521		return this.contactJid;
 522	}
 523
 524	public int getStatus() {
 525		return this.status;
 526	}
 527
 528	public long getCreated() {
 529		return this.created;
 530	}
 531
 532	public ContentValues getContentValues() {
 533		ContentValues values = new ContentValues();
 534		values.put(UUID, uuid);
 535		values.put(NAME, name);
 536		values.put(CONTACT, contactUuid);
 537		values.put(ACCOUNT, accountUuid);
 538		values.put(CONTACTJID, contactJid.toPreppedString());
 539		values.put(CREATED, created);
 540		values.put(STATUS, status);
 541		values.put(MODE, mode);
 542		values.put(ATTRIBUTES, attributes.toString());
 543		return values;
 544	}
 545
 546	public static Conversation fromCursor(Cursor cursor) {
 547		Jid jid;
 548		try {
 549			jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
 550		} catch (final InvalidJidException e) {
 551			// Borked DB..
 552			jid = null;
 553		}
 554		return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
 555				cursor.getString(cursor.getColumnIndex(NAME)),
 556				cursor.getString(cursor.getColumnIndex(CONTACT)),
 557				cursor.getString(cursor.getColumnIndex(ACCOUNT)),
 558				jid,
 559				cursor.getLong(cursor.getColumnIndex(CREATED)),
 560				cursor.getInt(cursor.getColumnIndex(STATUS)),
 561				cursor.getInt(cursor.getColumnIndex(MODE)),
 562				cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
 563	}
 564
 565	public void setStatus(int status) {
 566		this.status = status;
 567	}
 568
 569	public int getMode() {
 570		return this.mode;
 571	}
 572
 573	public void setMode(int mode) {
 574		this.mode = mode;
 575	}
 576
 577	public SessionImpl startOtrSession(String presence, boolean sendStart) {
 578		if (this.otrSession != null) {
 579			return this.otrSession;
 580		} else {
 581			final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
 582					presence,
 583					"xmpp");
 584			this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
 585			try {
 586				if (sendStart) {
 587					this.otrSession.startSession();
 588					return this.otrSession;
 589				}
 590				return this.otrSession;
 591			} catch (OtrException e) {
 592				return null;
 593			}
 594		}
 595
 596	}
 597
 598	public SessionImpl getOtrSession() {
 599		return this.otrSession;
 600	}
 601
 602	public void resetOtrSession() {
 603		this.otrFingerprint = null;
 604		this.otrSession = null;
 605		this.mSmp.hint = null;
 606		this.mSmp.secret = null;
 607		this.mSmp.status = Smp.STATUS_NONE;
 608	}
 609
 610	public Smp smp() {
 611		return mSmp;
 612	}
 613
 614	public boolean startOtrIfNeeded() {
 615		if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
 616			try {
 617				this.otrSession.startSession();
 618				return true;
 619			} catch (OtrException e) {
 620				this.resetOtrSession();
 621				return false;
 622			}
 623		} else {
 624			return true;
 625		}
 626	}
 627
 628	public boolean endOtrIfNeeded() {
 629		if (this.otrSession != null) {
 630			if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
 631				try {
 632					this.otrSession.endSession();
 633					this.resetOtrSession();
 634					return true;
 635				} catch (OtrException e) {
 636					this.resetOtrSession();
 637					return false;
 638				}
 639			} else {
 640				this.resetOtrSession();
 641				return false;
 642			}
 643		} else {
 644			return false;
 645		}
 646	}
 647
 648	public boolean hasValidOtrSession() {
 649		return this.otrSession != null;
 650	}
 651
 652	public synchronized String getOtrFingerprint() {
 653		if (this.otrFingerprint == null) {
 654			try {
 655				if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
 656					return null;
 657				}
 658				DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
 659				this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey).toLowerCase(Locale.US);
 660			} catch (final OtrCryptoException | UnsupportedOperationException ignored) {
 661				return null;
 662			}
 663		}
 664		return this.otrFingerprint;
 665	}
 666
 667	public boolean verifyOtrFingerprint() {
 668		final String fingerprint = getOtrFingerprint();
 669		if (fingerprint != null) {
 670			getContact().addOtrFingerprint(fingerprint);
 671			return true;
 672		} else {
 673			return false;
 674		}
 675	}
 676
 677	public boolean isOtrFingerprintVerified() {
 678		return getContact().getOtrFingerprints().contains(getOtrFingerprint());
 679	}
 680
 681	/**
 682	 * short for is Private and Non-anonymous
 683	 */
 684	private boolean isPnNA() {
 685		return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
 686	}
 687
 688	public synchronized MucOptions getMucOptions() {
 689		if (this.mucOptions == null) {
 690			this.mucOptions = new MucOptions(this);
 691		}
 692		return this.mucOptions;
 693	}
 694
 695	public void resetMucOptions() {
 696		this.mucOptions = null;
 697	}
 698
 699	public void setContactJid(final Jid jid) {
 700		this.contactJid = jid;
 701	}
 702
 703	public void setNextCounterpart(Jid jid) {
 704		this.nextCounterpart = jid;
 705	}
 706
 707	public Jid getNextCounterpart() {
 708		return this.nextCounterpart;
 709	}
 710
 711	public int getNextEncryption() {
 712		return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption()));
 713	}
 714
 715	private int fixAvailableEncryption(int selectedEncryption) {
 716		switch(selectedEncryption) {
 717			case Message.ENCRYPTION_NONE:
 718				return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption();
 719			case Message.ENCRYPTION_AXOLOTL:
 720				return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption();
 721			case Message.ENCRYPTION_OTR:
 722				return Config.supportOtr() ? selectedEncryption : getDefaultEncryption();
 723			case Message.ENCRYPTION_PGP:
 724			case Message.ENCRYPTION_DECRYPTED:
 725			case Message.ENCRYPTION_DECRYPTION_FAILED:
 726				return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption();
 727			default:
 728				return getDefaultEncryption();
 729		}
 730	}
 731
 732	private int getDefaultEncryption() {
 733		AxolotlService axolotlService = account.getAxolotlService();
 734		if (Config.supportUnencrypted()) {
 735			return Message.ENCRYPTION_NONE;
 736		} else if (Config.supportOmemo()
 737				&& (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) {
 738			return Message.ENCRYPTION_AXOLOTL;
 739		} else if (Config.supportOtr() && mode == MODE_SINGLE) {
 740			return Message.ENCRYPTION_OTR;
 741		} else if (Config.supportOpenPgp()) {
 742			return Message.ENCRYPTION_PGP;
 743		} else {
 744			return Message.ENCRYPTION_NONE;
 745		}
 746	}
 747
 748	public void setNextEncryption(int encryption) {
 749		this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
 750	}
 751
 752	public String getNextMessage() {
 753		if (this.nextMessage == null) {
 754			return "";
 755		} else {
 756			return this.nextMessage;
 757		}
 758	}
 759
 760	public boolean smpRequested() {
 761		return smp().status == Smp.STATUS_CONTACT_REQUESTED;
 762	}
 763
 764	public void setNextMessage(String message) {
 765		this.nextMessage = message;
 766	}
 767
 768	public void setSymmetricKey(byte[] key) {
 769		this.symmetricKey = key;
 770	}
 771
 772	public byte[] getSymmetricKey() {
 773		return this.symmetricKey;
 774	}
 775
 776	public void setBookmark(Bookmark bookmark) {
 777		this.bookmark = bookmark;
 778		this.bookmark.setConversation(this);
 779	}
 780
 781	public void deregisterWithBookmark() {
 782		if (this.bookmark != null) {
 783			this.bookmark.setConversation(null);
 784		}
 785		this.bookmark = null;
 786	}
 787
 788	public Bookmark getBookmark() {
 789		return this.bookmark;
 790	}
 791
 792	public boolean hasDuplicateMessage(Message message) {
 793		synchronized (this.messages) {
 794			for (int i = this.messages.size() - 1; i >= 0; --i) {
 795				if (this.messages.get(i).similar(message)) {
 796					return true;
 797				}
 798			}
 799		}
 800		return false;
 801	}
 802
 803	public Message findSentMessageWithBody(String body) {
 804		synchronized (this.messages) {
 805			for (int i = this.messages.size() - 1; i >= 0; --i) {
 806				Message message = this.messages.get(i);
 807				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 808					String otherBody;
 809					if (message.hasFileOnRemoteHost()) {
 810						otherBody = message.getFileParams().url.toString();
 811					} else {
 812						otherBody = message.body;
 813					}
 814					if (otherBody != null && otherBody.equals(body)) {
 815						return message;
 816					}
 817				}
 818			}
 819			return null;
 820		}
 821	}
 822
 823	public long getLastMessageTransmitted() {
 824		final long last_clear = getLastClearHistory();
 825		long last_received = 0;
 826		synchronized (this.messages) {
 827			for(int i = this.messages.size() - 1; i >= 0; --i) {
 828				Message message = this.messages.get(i);
 829				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
 830					last_received = message.getTimeSent();
 831					break;
 832				}
 833			}
 834		}
 835		return Math.max(last_clear,last_received);
 836	}
 837
 838	public void setMutedTill(long value) {
 839		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 840	}
 841
 842	public boolean isMuted() {
 843		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 844	}
 845
 846	public boolean alwaysNotify() {
 847		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
 848	}
 849
 850	public boolean setAttribute(String key, String value) {
 851		synchronized (this.attributes) {
 852			try {
 853				this.attributes.put(key, value);
 854				return true;
 855			} catch (JSONException e) {
 856				return false;
 857			}
 858		}
 859	}
 860
 861	public boolean setAttribute(String key, List<Jid> jids) {
 862		JSONArray array = new JSONArray();
 863		for(Jid jid : jids) {
 864			array.put(jid.toBareJid().toString());
 865		}
 866		synchronized (this.attributes) {
 867			try {
 868				this.attributes.put(key, array);
 869				return true;
 870			} catch (JSONException e) {
 871				e.printStackTrace();
 872				return false;
 873			}
 874		}
 875	}
 876
 877	public String getAttribute(String key) {
 878		synchronized (this.attributes) {
 879			try {
 880				return this.attributes.getString(key);
 881			} catch (JSONException e) {
 882				return null;
 883			}
 884		}
 885	}
 886
 887	private List<Jid> getJidListAttribute(String key) {
 888		ArrayList<Jid> list = new ArrayList<>();
 889		synchronized (this.attributes) {
 890			try {
 891				JSONArray array = this.attributes.getJSONArray(key);
 892				for (int i = 0; i < array.length(); ++i) {
 893					try {
 894						list.add(Jid.fromString(array.getString(i)));
 895					} catch (InvalidJidException e) {
 896						//ignored
 897					}
 898				}
 899			} catch (JSONException e) {
 900				//ignored
 901			}
 902		}
 903		return list;
 904	}
 905
 906	private int getIntAttribute(String key, int defaultValue) {
 907		String value = this.getAttribute(key);
 908		if (value == null) {
 909			return defaultValue;
 910		} else {
 911			try {
 912				return Integer.parseInt(value);
 913			} catch (NumberFormatException e) {
 914				return defaultValue;
 915			}
 916		}
 917	}
 918
 919	public long getLongAttribute(String key, long defaultValue) {
 920		String value = this.getAttribute(key);
 921		if (value == null) {
 922			return defaultValue;
 923		} else {
 924			try {
 925				return Long.parseLong(value);
 926			} catch (NumberFormatException e) {
 927				return defaultValue;
 928			}
 929		}
 930	}
 931
 932	private boolean getBooleanAttribute(String key, boolean defaultValue) {
 933		String value = this.getAttribute(key);
 934		if (value == null) {
 935			return defaultValue;
 936		} else {
 937			return Boolean.parseBoolean(value);
 938		}
 939	}
 940
 941	public void add(Message message) {
 942		message.setConversation(this);
 943		synchronized (this.messages) {
 944			this.messages.add(message);
 945		}
 946	}
 947
 948	public void prepend(Message message) {
 949		message.setConversation(this);
 950		synchronized (this.messages) {
 951			this.messages.add(0,message);
 952		}
 953	}
 954
 955	public void addAll(int index, List<Message> messages) {
 956		synchronized (this.messages) {
 957			this.messages.addAll(index, messages);
 958		}
 959		account.getPgpDecryptionService().decrypt(messages);
 960	}
 961
 962	public void expireOldMessages(long timestamp) {
 963		synchronized (this.messages) {
 964			for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
 965				if (iterator.next().getTimeSent() < timestamp) {
 966					iterator.remove();
 967				}
 968			}
 969			untieMessages();
 970		}
 971	}
 972
 973	public void sort() {
 974		synchronized (this.messages) {
 975			Collections.sort(this.messages, new Comparator<Message>() {
 976				@Override
 977				public int compare(Message left, Message right) {
 978					if (left.getTimeSent() < right.getTimeSent()) {
 979						return -1;
 980					} else if (left.getTimeSent() > right.getTimeSent()) {
 981						return 1;
 982					} else {
 983						return 0;
 984					}
 985				}
 986			});
 987			untieMessages();
 988		}
 989	}
 990
 991	private void untieMessages() {
 992		for(Message message : this.messages) {
 993			message.untie();
 994		}
 995	}
 996
 997	public int unreadCount() {
 998		synchronized (this.messages) {
 999			int count = 0;
1000			for(int i = this.messages.size() - 1; i >= 0; --i) {
1001				if (this.messages.get(i).isRead()) {
1002					return count;
1003				}
1004				++count;
1005			}
1006			return count;
1007		}
1008	}
1009
1010	private int sentMessagesCount() {
1011		int count = 0;
1012		synchronized (this.messages) {
1013			for(Message message : messages) {
1014				if (message.getStatus() != Message.STATUS_RECEIVED) {
1015					++count;
1016				}
1017			}
1018		}
1019		return count;
1020	}
1021
1022	public boolean isWithStranger() {
1023		return mode == MODE_SINGLE
1024				&& !getJid().equals(account.getJid().toDomainJid())
1025				&& !getContact().showInRoster()
1026				&& sentMessagesCount() == 0;
1027	}
1028
1029	public class Smp {
1030		public static final int STATUS_NONE = 0;
1031		public static final int STATUS_CONTACT_REQUESTED = 1;
1032		public static final int STATUS_WE_REQUESTED = 2;
1033		public static final int STATUS_FAILED = 3;
1034		public static final int STATUS_VERIFIED = 4;
1035
1036		public String secret = null;
1037		public String hint = null;
1038		public int status = 0;
1039	}
1040}