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	}
 786
 787	public Bookmark getBookmark() {
 788		return this.bookmark;
 789	}
 790
 791	public boolean hasDuplicateMessage(Message message) {
 792		synchronized (this.messages) {
 793			for (int i = this.messages.size() - 1; i >= 0; --i) {
 794				if (this.messages.get(i).similar(message)) {
 795					return true;
 796				}
 797			}
 798		}
 799		return false;
 800	}
 801
 802	public Message findSentMessageWithBody(String body) {
 803		synchronized (this.messages) {
 804			for (int i = this.messages.size() - 1; i >= 0; --i) {
 805				Message message = this.messages.get(i);
 806				if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
 807					String otherBody;
 808					if (message.hasFileOnRemoteHost()) {
 809						otherBody = message.getFileParams().url.toString();
 810					} else {
 811						otherBody = message.body;
 812					}
 813					if (otherBody != null && otherBody.equals(body)) {
 814						return message;
 815					}
 816				}
 817			}
 818			return null;
 819		}
 820	}
 821
 822	public long getLastMessageTransmitted() {
 823		final long last_clear = getLastClearHistory();
 824		long last_received = 0;
 825		synchronized (this.messages) {
 826			for(int i = this.messages.size() - 1; i >= 0; --i) {
 827				Message message = this.messages.get(i);
 828				if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
 829					last_received = message.getTimeSent();
 830					break;
 831				}
 832			}
 833		}
 834		return Math.max(last_clear,last_received);
 835	}
 836
 837	public void setMutedTill(long value) {
 838		this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
 839	}
 840
 841	public boolean isMuted() {
 842		return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
 843	}
 844
 845	public boolean alwaysNotify() {
 846		return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
 847	}
 848
 849	public boolean setAttribute(String key, String value) {
 850		synchronized (this.attributes) {
 851			try {
 852				this.attributes.put(key, value);
 853				return true;
 854			} catch (JSONException e) {
 855				return false;
 856			}
 857		}
 858	}
 859
 860	public boolean setAttribute(String key, List<Jid> jids) {
 861		JSONArray array = new JSONArray();
 862		for(Jid jid : jids) {
 863			array.put(jid.toBareJid().toString());
 864		}
 865		synchronized (this.attributes) {
 866			try {
 867				this.attributes.put(key, array);
 868				return true;
 869			} catch (JSONException e) {
 870				e.printStackTrace();
 871				return false;
 872			}
 873		}
 874	}
 875
 876	public String getAttribute(String key) {
 877		synchronized (this.attributes) {
 878			try {
 879				return this.attributes.getString(key);
 880			} catch (JSONException e) {
 881				return null;
 882			}
 883		}
 884	}
 885
 886	private List<Jid> getJidListAttribute(String key) {
 887		ArrayList<Jid> list = new ArrayList<>();
 888		synchronized (this.attributes) {
 889			try {
 890				JSONArray array = this.attributes.getJSONArray(key);
 891				for (int i = 0; i < array.length(); ++i) {
 892					try {
 893						list.add(Jid.fromString(array.getString(i)));
 894					} catch (InvalidJidException e) {
 895						//ignored
 896					}
 897				}
 898			} catch (JSONException e) {
 899				//ignored
 900			}
 901		}
 902		return list;
 903	}
 904
 905	private int getIntAttribute(String key, int defaultValue) {
 906		String value = this.getAttribute(key);
 907		if (value == null) {
 908			return defaultValue;
 909		} else {
 910			try {
 911				return Integer.parseInt(value);
 912			} catch (NumberFormatException e) {
 913				return defaultValue;
 914			}
 915		}
 916	}
 917
 918	public long getLongAttribute(String key, long defaultValue) {
 919		String value = this.getAttribute(key);
 920		if (value == null) {
 921			return defaultValue;
 922		} else {
 923			try {
 924				return Long.parseLong(value);
 925			} catch (NumberFormatException e) {
 926				return defaultValue;
 927			}
 928		}
 929	}
 930
 931	private boolean getBooleanAttribute(String key, boolean defaultValue) {
 932		String value = this.getAttribute(key);
 933		if (value == null) {
 934			return defaultValue;
 935		} else {
 936			return Boolean.parseBoolean(value);
 937		}
 938	}
 939
 940	public void add(Message message) {
 941		message.setConversation(this);
 942		synchronized (this.messages) {
 943			this.messages.add(message);
 944		}
 945	}
 946
 947	public void prepend(Message message) {
 948		message.setConversation(this);
 949		synchronized (this.messages) {
 950			this.messages.add(0,message);
 951		}
 952	}
 953
 954	public void addAll(int index, List<Message> messages) {
 955		synchronized (this.messages) {
 956			this.messages.addAll(index, messages);
 957		}
 958		account.getPgpDecryptionService().decrypt(messages);
 959	}
 960
 961	public void expireOldMessages(long timestamp) {
 962		synchronized (this.messages) {
 963			for(ListIterator<Message> iterator = this.messages.listIterator(); iterator.hasNext();) {
 964				if (iterator.next().getTimeSent() < timestamp) {
 965					iterator.remove();
 966				}
 967			}
 968			untieMessages();
 969		}
 970	}
 971
 972	public void sort() {
 973		synchronized (this.messages) {
 974			Collections.sort(this.messages, new Comparator<Message>() {
 975				@Override
 976				public int compare(Message left, Message right) {
 977					if (left.getTimeSent() < right.getTimeSent()) {
 978						return -1;
 979					} else if (left.getTimeSent() > right.getTimeSent()) {
 980						return 1;
 981					} else {
 982						return 0;
 983					}
 984				}
 985			});
 986			untieMessages();
 987		}
 988	}
 989
 990	private void untieMessages() {
 991		for(Message message : this.messages) {
 992			message.untie();
 993		}
 994	}
 995
 996	public int unreadCount() {
 997		synchronized (this.messages) {
 998			int count = 0;
 999			for(int i = this.messages.size() - 1; i >= 0; --i) {
1000				if (this.messages.get(i).isRead()) {
1001					return count;
1002				}
1003				++count;
1004			}
1005			return count;
1006		}
1007	}
1008
1009	private int sentMessagesCount() {
1010		int count = 0;
1011		synchronized (this.messages) {
1012			for(Message message : messages) {
1013				if (message.getStatus() != Message.STATUS_RECEIVED) {
1014					++count;
1015				}
1016			}
1017		}
1018		return count;
1019	}
1020
1021	public boolean isWithStranger() {
1022		return mode == MODE_SINGLE
1023				&& !getJid().equals(account.getJid().toDomainJid())
1024				&& !getContact().showInRoster()
1025				&& sentMessagesCount() == 0;
1026	}
1027
1028	public class Smp {
1029		public static final int STATUS_NONE = 0;
1030		public static final int STATUS_CONTACT_REQUESTED = 1;
1031		public static final int STATUS_WE_REQUESTED = 2;
1032		public static final int STATUS_FAILED = 3;
1033		public static final int STATUS_VERIFIED = 4;
1034
1035		public String secret = null;
1036		public String hint = null;
1037		public int status = 0;
1038	}
1039}