aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/de/pixart/messenger/entities
diff options
context:
space:
mode:
authorChristian Schneppe <christian@pix-art.de>2016-07-29 19:52:37 +0200
committerChristian Schneppe <christian@pix-art.de>2016-07-29 19:52:37 +0200
commit94933e21cd08c53a23e5ec6c12bc1dc383b1f3ce (patch)
tree4fa096547d0917f603252c9a279e4208ba4ef711 /src/main/java/de/pixart/messenger/entities
parent50889004f3c679387d95ba9c49a53a8f882ba33c (diff)
changed package id inside manifest and project
Diffstat (limited to 'src/main/java/de/pixart/messenger/entities')
-rw-r--r--src/main/java/de/pixart/messenger/entities/AbstractEntity.java20
-rw-r--r--src/main/java/de/pixart/messenger/entities/Account.java604
-rw-r--r--src/main/java/de/pixart/messenger/entities/Blockable.java11
-rw-r--r--src/main/java/de/pixart/messenger/entities/Bookmark.java171
-rw-r--r--src/main/java/de/pixart/messenger/entities/Contact.java547
-rw-r--r--src/main/java/de/pixart/messenger/entities/Conversation.java1007
-rw-r--r--src/main/java/de/pixart/messenger/entities/DownloadableFile.java83
-rw-r--r--src/main/java/de/pixart/messenger/entities/ListItem.java37
-rw-r--r--src/main/java/de/pixart/messenger/entities/Message.java831
-rw-r--r--src/main/java/de/pixart/messenger/entities/MucOptions.java680
-rw-r--r--src/main/java/de/pixart/messenger/entities/Presence.java92
-rw-r--r--src/main/java/de/pixart/messenger/entities/PresenceTemplate.java76
-rw-r--r--src/main/java/de/pixart/messenger/entities/Presences.java102
-rw-r--r--src/main/java/de/pixart/messenger/entities/Roster.java96
-rw-r--r--src/main/java/de/pixart/messenger/entities/ServiceDiscoveryResult.java349
-rw-r--r--src/main/java/de/pixart/messenger/entities/Transferable.java78
-rw-r--r--src/main/java/de/pixart/messenger/entities/TransferablePlaceholder.java34
17 files changed, 4818 insertions, 0 deletions
diff --git a/src/main/java/de/pixart/messenger/entities/AbstractEntity.java b/src/main/java/de/pixart/messenger/entities/AbstractEntity.java
new file mode 100644
index 000000000..1194cb819
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/AbstractEntity.java
@@ -0,0 +1,20 @@
+package de.pixart.messenger.entities;
+
+import android.content.ContentValues;
+
+public abstract class AbstractEntity {
+
+ public static final String UUID = "uuid";
+
+ protected String uuid;
+
+ public String getUuid() {
+ return this.uuid;
+ }
+
+ public abstract ContentValues getContentValues();
+
+ public boolean equals(AbstractEntity entity) {
+ return this.getUuid().equals(entity.getUuid());
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Account.java b/src/main/java/de/pixart/messenger/entities/Account.java
new file mode 100644
index 000000000..8c9ed51f2
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Account.java
@@ -0,0 +1,604 @@
+package de.pixart.messenger.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.os.SystemClock;
+import android.util.Pair;
+
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.security.PublicKey;
+import java.security.interfaces.DSAPublicKey;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+import de.pixart.messenger.R;
+import de.pixart.messenger.crypto.OtrService;
+import de.pixart.messenger.crypto.PgpDecryptionService;
+import de.pixart.messenger.crypto.axolotl.AxolotlService;
+import de.pixart.messenger.services.XmppConnectionService;
+import de.pixart.messenger.xmpp.XmppConnection;
+import de.pixart.messenger.xmpp.jid.InvalidJidException;
+import de.pixart.messenger.xmpp.jid.Jid;
+
+public class Account extends AbstractEntity {
+
+ public static final String TABLENAME = "accounts";
+
+ public static final String USERNAME = "username";
+ public static final String SERVER = "server";
+ public static final String PASSWORD = "password";
+ public static final String OPTIONS = "options";
+ public static final String ROSTERVERSION = "rosterversion";
+ public static final String KEYS = "keys";
+ public static final String AVATAR = "avatar";
+ public static final String DISPLAY_NAME = "display_name";
+ public static final String HOSTNAME = "hostname";
+ public static final String PORT = "port";
+ public static final String STATUS = "status";
+ public static final String STATUS_MESSAGE = "status_message";
+
+ public static final String PINNED_MECHANISM_KEY = "pinned_mechanism";
+
+ public static final int OPTION_USETLS = 0;
+ public static final int OPTION_DISABLED = 1;
+ public static final int OPTION_REGISTER = 2;
+ public static final int OPTION_USECOMPRESSION = 3;
+ public static final int OPTION_MAGIC_CREATE = 4;
+ public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>();
+
+ public boolean httpUploadAvailable(long filesize) {
+ return xmppConnection != null && xmppConnection.getFeatures().httpUpload(filesize);
+ }
+
+ public boolean httpUploadAvailable() {
+ return httpUploadAvailable(0);
+ }
+
+ public void setDisplayName(String displayName) {
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public XmppConnection.Identity getServerIdentity() {
+ if (xmppConnection == null) {
+ return XmppConnection.Identity.UNKNOWN;
+ } else {
+ return xmppConnection.getServerIdentity();
+ }
+ }
+
+ public Contact getSelfContact() {
+ return getRoster().getContact(jid);
+ }
+
+ public boolean hasPendingPgpIntent(Conversation conversation) {
+ return pgpDecryptionService != null && pgpDecryptionService.hasPendingIntent(conversation);
+ }
+
+ public boolean isPgpDecryptionServiceConnected() {
+ return pgpDecryptionService != null && pgpDecryptionService.isConnected();
+ }
+
+ public enum State {
+ DISABLED,
+ OFFLINE,
+ CONNECTING,
+ ONLINE,
+ NO_INTERNET,
+ UNAUTHORIZED(true),
+ SERVER_NOT_FOUND(true),
+ REGISTRATION_FAILED(true),
+ REGISTRATION_CONFLICT(true),
+ REGISTRATION_SUCCESSFUL,
+ REGISTRATION_NOT_SUPPORTED(true),
+ SECURITY_ERROR(true),
+ INCOMPATIBLE_SERVER(true),
+ TOR_NOT_AVAILABLE(true),
+ BIND_FAILURE(true),
+ HOST_UNKNOWN(true),
+ REGISTRATION_PLEASE_WAIT(true),
+ STREAM_ERROR(true),
+ POLICY_VIOLATION(true),
+ REGISTRATION_PASSWORD_TOO_WEAK(true);
+
+ private final boolean isError;
+
+ public boolean isError() {
+ return this.isError;
+ }
+
+ State(final boolean isError) {
+ this.isError = isError;
+ }
+
+ State() {
+ this(false);
+ }
+
+ public int getReadableId() {
+ switch (this) {
+ case DISABLED:
+ return R.string.account_status_disabled;
+ case ONLINE:
+ return R.string.account_status_online;
+ case CONNECTING:
+ return R.string.account_status_connecting;
+ case OFFLINE:
+ return R.string.account_status_offline;
+ case UNAUTHORIZED:
+ return R.string.account_status_unauthorized;
+ case SERVER_NOT_FOUND:
+ return R.string.account_status_not_found;
+ case NO_INTERNET:
+ return R.string.account_status_no_internet;
+ case REGISTRATION_FAILED:
+ return R.string.account_status_regis_fail;
+ case REGISTRATION_CONFLICT:
+ return R.string.account_status_regis_conflict;
+ case REGISTRATION_SUCCESSFUL:
+ return R.string.account_status_regis_success;
+ case REGISTRATION_NOT_SUPPORTED:
+ return R.string.account_status_regis_not_sup;
+ case SECURITY_ERROR:
+ return R.string.account_status_security_error;
+ case INCOMPATIBLE_SERVER:
+ return R.string.account_status_incompatible_server;
+ case TOR_NOT_AVAILABLE:
+ return R.string.account_status_tor_unavailable;
+ case BIND_FAILURE:
+ return R.string.account_status_bind_failure;
+ case HOST_UNKNOWN:
+ return R.string.account_status_host_unknown;
+ case POLICY_VIOLATION:
+ return R.string.account_status_policy_violation;
+ case REGISTRATION_PLEASE_WAIT:
+ return R.string.registration_please_wait;
+ case REGISTRATION_PASSWORD_TOO_WEAK:
+ return R.string.registration_password_too_weak;
+ case STREAM_ERROR:
+ return R.string.account_status_stream_error;
+ default:
+ return R.string.account_status_unknown;
+ }
+ }
+ }
+
+ public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>();
+ public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>();
+
+ private static final String KEY_PGP_SIGNATURE = "pgp_signature";
+ private static final String KEY_PGP_ID = "pgp_id";
+
+ protected Jid jid;
+ protected String password;
+ protected int options = 0;
+ protected String rosterVersion;
+ protected State status = State.OFFLINE;
+ protected JSONObject keys = new JSONObject();
+ protected String avatar;
+ protected String displayName = null;
+ protected String hostname = null;
+ protected int port = 5222;
+ protected boolean online = false;
+ private OtrService mOtrService = null;
+ private AxolotlService axolotlService = null;
+ private PgpDecryptionService pgpDecryptionService = null;
+ private XmppConnection xmppConnection = null;
+ private long mEndGracePeriod = 0L;
+ private String otrFingerprint;
+ private final Roster roster = new Roster(this);
+ private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>();
+ private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>();
+ private Presence.Status presenceStatus = Presence.Status.ONLINE;
+ private String presenceStatusMessage = null;
+
+ public Account(final Jid jid, final String password) {
+ this(java.util.UUID.randomUUID().toString(), jid,
+ password, 0, null, "", null, null, null, 5222, Presence.Status.ONLINE, null);
+ }
+
+ private Account(final String uuid, final Jid jid,
+ final String password, final int options, final String rosterVersion, final String keys,
+ final String avatar, String displayName, String hostname, int port,
+ final Presence.Status status, String statusMessage) {
+ this.uuid = uuid;
+ this.jid = jid;
+ if (jid.isBareJid()) {
+ this.setResource("mobile");
+ }
+ this.password = password;
+ this.options = options;
+ this.rosterVersion = rosterVersion;
+ try {
+ this.keys = new JSONObject(keys);
+ } catch (final JSONException ignored) {
+ this.keys = new JSONObject();
+ }
+ this.avatar = avatar;
+ this.displayName = displayName;
+ this.hostname = hostname;
+ this.port = port;
+ this.presenceStatus = status;
+ this.presenceStatusMessage = statusMessage;
+ }
+
+ public static Account fromCursor(final Cursor cursor) {
+ Jid jid = null;
+ try {
+ jid = Jid.fromParts(cursor.getString(cursor.getColumnIndex(USERNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVER)), "mobile");
+ } catch (final InvalidJidException ignored) {
+ }
+ return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
+ jid,
+ cursor.getString(cursor.getColumnIndex(PASSWORD)),
+ cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+ cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
+ cursor.getString(cursor.getColumnIndex(KEYS)),
+ cursor.getString(cursor.getColumnIndex(AVATAR)),
+ cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)),
+ cursor.getString(cursor.getColumnIndex(HOSTNAME)),
+ cursor.getInt(cursor.getColumnIndex(PORT)),
+ Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS))),
+ cursor.getString(cursor.getColumnIndex(STATUS_MESSAGE)));
+ }
+
+ public boolean isOptionSet(final int option) {
+ return ((options & (1 << option)) != 0);
+ }
+
+ public void setOption(final int option, final boolean value) {
+ if (value) {
+ this.options |= 1 << option;
+ } else {
+ this.options &= ~(1 << option);
+ }
+ }
+
+ public String getUsername() {
+ return jid.getLocalpart();
+ }
+
+ public void setJid(final Jid jid) {
+ this.jid = jid;
+ }
+
+ public Jid getServer() {
+ return jid.toDomainJid();
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(final String password) {
+ this.password = password;
+ }
+
+ public void setHostname(String hostname) {
+ this.hostname = hostname;
+ }
+
+ public String getHostname() {
+ return this.hostname == null ? "" : this.hostname;
+ }
+
+ public boolean isOnion() {
+ return getServer().toString().toLowerCase().endsWith(".onion");
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ public int getPort() {
+ return this.port;
+ }
+
+ public State getStatus() {
+ if (isOptionSet(OPTION_DISABLED)) {
+ return State.DISABLED;
+ } else {
+ return this.status;
+ }
+ }
+
+ public void setStatus(final State status) {
+ this.status = status;
+ }
+
+ public boolean errorStatus() {
+ return getStatus().isError();
+ }
+
+ public boolean hasErrorStatus() {
+ return getXmppConnection() != null
+ && (getStatus().isError() || getStatus() == State.CONNECTING)
+ && getXmppConnection().getAttempt() >= 3;
+ }
+
+ public void setPresenceStatus(Presence.Status status) {
+ this.presenceStatus = status;
+ }
+
+ public Presence.Status getPresenceStatus() {
+ return this.presenceStatus;
+ }
+
+ public void setPresenceStatusMessage(String message) {
+ this.presenceStatusMessage = message;
+ }
+
+ public String getPresenceStatusMessage() {
+ return this.presenceStatusMessage;
+ }
+
+ public String getResource() {
+ return jid.getResourcepart();
+ }
+
+ public boolean setResource(final String resource) {
+ final String oldResource = jid.getResourcepart();
+ if (oldResource == null || !oldResource.equals(resource)) {
+ try {
+ jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource);
+ return true;
+ } catch (final InvalidJidException ignored) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Jid getJid() {
+ return jid;
+ }
+
+ public JSONObject getKeys() {
+ return keys;
+ }
+
+ public String getKey(final String name) {
+ return this.keys.optString(name, null);
+ }
+
+ public boolean setKey(final String keyName, final String keyValue) {
+ try {
+ this.keys.put(keyName, keyValue);
+ return true;
+ } catch (final JSONException e) {
+ return false;
+ }
+ }
+
+ public boolean setPrivateKeyAlias(String alias) {
+ return setKey("private_key_alias", alias);
+ }
+
+ public String getPrivateKeyAlias() {
+ return getKey("private_key_alias");
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ final ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(USERNAME, jid.getLocalpart());
+ values.put(SERVER, jid.getDomainpart());
+ values.put(PASSWORD, password);
+ values.put(OPTIONS, options);
+ values.put(KEYS, this.keys.toString());
+ values.put(ROSTERVERSION, rosterVersion);
+ values.put(AVATAR, avatar);
+ values.put(DISPLAY_NAME, displayName);
+ values.put(HOSTNAME, hostname);
+ values.put(PORT, port);
+ values.put(STATUS, presenceStatus.toShowString());
+ values.put(STATUS_MESSAGE, presenceStatusMessage);
+ return values;
+ }
+
+ public AxolotlService getAxolotlService() {
+ return axolotlService;
+ }
+
+ public void initAccountServices(final XmppConnectionService context) {
+ this.mOtrService = new OtrService(context, this);
+ this.axolotlService = new AxolotlService(this, context);
+ this.pgpDecryptionService = new PgpDecryptionService(context);
+ if (xmppConnection != null) {
+ xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService);
+ }
+ }
+
+ public OtrService getOtrService() {
+ return this.mOtrService;
+ }
+
+ public PgpDecryptionService getPgpDecryptionService() {
+ return this.pgpDecryptionService;
+ }
+
+ public XmppConnection getXmppConnection() {
+ return this.xmppConnection;
+ }
+
+ public void setXmppConnection(final XmppConnection connection) {
+ this.xmppConnection = connection;
+ }
+
+ public String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ if (this.mOtrService == null) {
+ return null;
+ }
+ final PublicKey publicKey = this.mOtrService.getPublicKey();
+ if (publicKey == null || !(publicKey instanceof DSAPublicKey)) {
+ return null;
+ }
+ this.otrFingerprint = new OtrCryptoEngineImpl().getFingerprint(publicKey);
+ return this.otrFingerprint;
+ } catch (final OtrCryptoException ignored) {
+ return null;
+ }
+ } else {
+ return this.otrFingerprint;
+ }
+ }
+
+ public String getRosterVersion() {
+ if (this.rosterVersion == null) {
+ return "";
+ } else {
+ return this.rosterVersion;
+ }
+ }
+
+ public void setRosterVersion(final String version) {
+ this.rosterVersion = version;
+ }
+
+ public int countPresences() {
+ return this.getSelfContact().getPresences().size();
+ }
+
+ public String getPgpSignature() {
+ try {
+ if (keys.has(KEY_PGP_SIGNATURE) && !"null".equals(keys.getString(KEY_PGP_SIGNATURE))) {
+ return keys.getString(KEY_PGP_SIGNATURE);
+ } else {
+ return null;
+ }
+ } catch (final JSONException e) {
+ return null;
+ }
+ }
+
+ public boolean setPgpSignature(String signature) {
+ try {
+ keys.put(KEY_PGP_SIGNATURE, signature);
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public boolean unsetPgpSignature() {
+ try {
+ keys.put(KEY_PGP_SIGNATURE, JSONObject.NULL);
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public long getPgpId() {
+ if (keys.has(KEY_PGP_ID)) {
+ try {
+ return keys.getLong(KEY_PGP_ID);
+ } catch (JSONException e) {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ public boolean setPgpSignId(long pgpID) {
+ try {
+ keys.put(KEY_PGP_ID, pgpID);
+ } catch (JSONException e) {
+ return false;
+ }
+ return true;
+ }
+
+ public Roster getRoster() {
+ return this.roster;
+ }
+
+ public List<Bookmark> getBookmarks() {
+ return this.bookmarks;
+ }
+
+ public void setBookmarks(final List<Bookmark> bookmarks) {
+ this.bookmarks = bookmarks;
+ }
+
+ public boolean hasBookmarkFor(final Jid conferenceJid) {
+ for (final Bookmark bookmark : this.bookmarks) {
+ final Jid jid = bookmark.getJid();
+ if (jid != null && jid.equals(conferenceJid.toBareJid())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean setAvatar(final String filename) {
+ if (this.avatar != null && this.avatar.equals(filename)) {
+ return false;
+ } else {
+ this.avatar = filename;
+ return true;
+ }
+ }
+
+ public String getAvatar() {
+ return this.avatar;
+ }
+
+ public void activateGracePeriod(long duration) {
+ this.mEndGracePeriod = SystemClock.elapsedRealtime() + duration;
+ }
+
+ public void deactivateGracePeriod() {
+ this.mEndGracePeriod = 0L;
+ }
+
+ public boolean inGracePeriod() {
+ return SystemClock.elapsedRealtime() < this.mEndGracePeriod;
+ }
+
+ public String getShareableUri() {
+ final String fingerprint = this.getOtrFingerprint();
+ if (fingerprint != null) {
+ return "xmpp:" + this.getJid().toBareJid().toString() + "?otr-fingerprint="+fingerprint;
+ } else {
+ return "xmpp:" + this.getJid().toBareJid().toString();
+ }
+ }
+
+ public boolean isBlocked(final ListItem contact) {
+ final Jid jid = contact.getJid();
+ return jid != null && (blocklist.contains(jid.toBareJid()) || blocklist.contains(jid.toDomainJid()));
+ }
+
+ public boolean isBlocked(final Jid jid) {
+ return jid != null && blocklist.contains(jid.toBareJid());
+ }
+
+ public Collection<Jid> getBlocklist() {
+ return this.blocklist;
+ }
+
+ public void clearBlocklist() {
+ getBlocklist().clear();
+ }
+
+ public boolean isOnlineAndConnected() {
+ return this.getStatus() == State.ONLINE && this.getXmppConnection() != null;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Blockable.java b/src/main/java/de/pixart/messenger/entities/Blockable.java
new file mode 100644
index 000000000..07860bcfe
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Blockable.java
@@ -0,0 +1,11 @@
+package de.pixart.messenger.entities;
+
+import de.pixart.messenger.xmpp.jid.Jid;
+
+public interface Blockable {
+ public boolean isBlocked();
+ public boolean isDomainBlocked();
+ public Jid getBlockedJid();
+ public Jid getJid();
+ public Account getAccount();
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Bookmark.java b/src/main/java/de/pixart/messenger/entities/Bookmark.java
new file mode 100644
index 000000000..9c99a2baf
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Bookmark.java
@@ -0,0 +1,171 @@
+package de.pixart.messenger.entities;
+
+import android.content.Context;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import de.pixart.messenger.utils.UIHelper;
+import de.pixart.messenger.xml.Element;
+import de.pixart.messenger.xmpp.jid.Jid;
+
+public class Bookmark extends Element implements ListItem {
+
+ private Account account;
+ private Conversation mJoinedConversation;
+
+ public Bookmark(final Account account, final Jid jid) {
+ super("conference");
+ this.setAttribute("jid", jid.toString());
+ this.account = account;
+ }
+
+ private Bookmark(Account account) {
+ super("conference");
+ this.account = account;
+ }
+
+ public static Bookmark parse(Element element, Account account) {
+ Bookmark bookmark = new Bookmark(account);
+ bookmark.setAttributes(element.getAttributes());
+ bookmark.setChildren(element.getChildren());
+ return bookmark;
+ }
+
+ public void setAutojoin(boolean autojoin) {
+ if (autojoin) {
+ this.setAttribute("autojoin", "true");
+ } else {
+ this.setAttribute("autojoin", "false");
+ }
+ }
+
+ @Override
+ public int compareTo(final ListItem another) {
+ return this.getDisplayName().compareToIgnoreCase(
+ another.getDisplayName());
+ }
+
+ @Override
+ public String getDisplayName() {
+ if (this.mJoinedConversation != null) {
+ return this.mJoinedConversation.getName();
+ } else if (getBookmarkName() != null
+ && !getBookmarkName().trim().isEmpty()) {
+ return getBookmarkName().trim();
+ } else {
+ Jid jid = this.getJid();
+ String name = jid != null ? jid.getLocalpart() : getAttribute("jid");
+ return name != null ? name : "";
+ }
+ }
+
+ @Override
+ public String getDisplayJid() {
+ Jid jid = getJid();
+ if (jid != null) {
+ return jid.toString();
+ } else {
+ return getAttribute("jid"); //fallback if jid wasn't parsable
+ }
+ }
+
+ @Override
+ public Jid getJid() {
+ return this.getAttributeAsJid("jid");
+ }
+
+ @Override
+ public List<Tag> getTags(Context context) {
+ ArrayList<Tag> tags = new ArrayList<>();
+ for (Element element : getChildren()) {
+ if (element.getName().equals("group") && element.getContent() != null) {
+ String group = element.getContent();
+ tags.add(new Tag(group, UIHelper.getColorForName(group)));
+ }
+ }
+ return tags;
+ }
+
+ public String getNick() {
+ return this.findChildContent("nick");
+ }
+
+ public void setNick(String nick) {
+ Element element = this.findChild("nick");
+ if (element == null) {
+ element = this.addChild("nick");
+ }
+ element.setContent(nick);
+ }
+
+ public boolean autojoin() {
+ return this.getAttributeAsBoolean("autojoin");
+ }
+
+ public String getPassword() {
+ return this.findChildContent("password");
+ }
+
+ public void setPassword(String password) {
+ Element element = this.findChild("password");
+ if (element != null) {
+ element.setContent(password);
+ }
+ }
+
+ @Override
+ public boolean match(Context context, String needle) {
+ if (needle == null) {
+ return true;
+ }
+ needle = needle.toLowerCase(Locale.US);
+ final Jid jid = getJid();
+ return (jid != null && jid.toString().contains(needle)) ||
+ getDisplayName().toLowerCase(Locale.US).contains(needle) ||
+ matchInTag(context, needle);
+ }
+
+ private boolean matchInTag(Context context, String needle) {
+ needle = needle.toLowerCase(Locale.US);
+ for (Tag tag : getTags(context)) {
+ if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Conversation getConversation() {
+ return this.mJoinedConversation;
+ }
+
+ public void setConversation(Conversation conversation) {
+ this.mJoinedConversation = conversation;
+ }
+
+ public String getBookmarkName() {
+ return this.getAttribute("name");
+ }
+
+ public boolean setBookmarkName(String name) {
+ String before = getBookmarkName();
+ if (name != null && !name.equals(before)) {
+ this.setAttribute("name", name);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void unregisterConversation() {
+ if (this.mJoinedConversation != null) {
+ this.mJoinedConversation.deregisterWithBookmark();
+ }
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Contact.java b/src/main/java/de/pixart/messenger/entities/Contact.java
new file mode 100644
index 000000000..b4908d08c
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Contact.java
@@ -0,0 +1,547 @@
+package de.pixart.messenger.entities;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+import de.pixart.messenger.Config;
+import de.pixart.messenger.utils.UIHelper;
+import de.pixart.messenger.xml.Element;
+import de.pixart.messenger.xmpp.jid.InvalidJidException;
+import de.pixart.messenger.xmpp.jid.Jid;
+import de.pixart.messenger.xmpp.pep.Avatar;
+
+public class Contact implements ListItem, Blockable {
+ public static final String TABLENAME = "contacts";
+
+ public static final String SYSTEMNAME = "systemname";
+ public static final String SERVERNAME = "servername";
+ public static final String JID = "jid";
+ public static final String OPTIONS = "options";
+ public static final String SYSTEMACCOUNT = "systemaccount";
+ public static final String PHOTOURI = "photouri";
+ public static final String KEYS = "pgpkey";
+ public static final String ACCOUNT = "accountUuid";
+ public static final String AVATAR = "avatar";
+ public static final String LAST_PRESENCE = "last_presence";
+ public static final String LAST_TIME = "last_time";
+ public static final String GROUPS = "groups";
+ protected String accountUuid;
+ protected String systemName;
+ protected String serverName;
+ protected String presenceName;
+ protected String commonName;
+ protected Jid jid;
+ protected int subscription = 0;
+ protected String systemAccount;
+ protected String photoUri;
+ protected JSONObject keys = new JSONObject();
+ protected JSONArray groups = new JSONArray();
+ protected final Presences presences = new Presences();
+ protected Account account;
+ protected Avatar avatar;
+
+ private boolean mActive = false;
+ private long mLastseen = 0;
+ private String mLastPresence = null;
+
+ public Contact(final String account, final String systemName, final String serverName,
+ final Jid jid, final int subscription, final String photoUri,
+ final String systemAccount, final String keys, final String avatar, final long lastseen,
+ final String presence, final String groups) {
+ this.accountUuid = account;
+ this.systemName = systemName;
+ this.serverName = serverName;
+ this.jid = jid;
+ this.subscription = subscription;
+ this.photoUri = photoUri;
+ this.systemAccount = systemAccount;
+ try {
+ this.keys = (keys == null ? new JSONObject("") : new JSONObject(keys));
+ } catch (JSONException e) {
+ this.keys = new JSONObject();
+ }
+ if (avatar != null) {
+ this.avatar = new Avatar();
+ this.avatar.sha1sum = avatar;
+ this.avatar.origin = Avatar.Origin.VCARD; //always assume worst
+ }
+ try {
+ this.groups = (groups == null ? new JSONArray() : new JSONArray(groups));
+ } catch (JSONException e) {
+ this.groups = new JSONArray();
+ }
+ this.mLastseen = lastseen;
+ this.mLastPresence = presence;
+ }
+
+ public Contact(final Jid jid) {
+ this.jid = jid;
+ }
+
+ public static Contact fromCursor(final Cursor cursor) {
+ final Jid jid;
+ try {
+ jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(JID)), true);
+ } catch (final InvalidJidException e) {
+ // TODO: Borked DB... handle this somehow?
+ return null;
+ }
+ return new Contact(cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(SYSTEMNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVERNAME)),
+ jid,
+ cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+ cursor.getString(cursor.getColumnIndex(PHOTOURI)),
+ cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(KEYS)),
+ cursor.getString(cursor.getColumnIndex(AVATAR)),
+ cursor.getLong(cursor.getColumnIndex(LAST_TIME)),
+ cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)),
+ cursor.getString(cursor.getColumnIndex(GROUPS)));
+ }
+
+ public String getDisplayName() {
+ if (this.commonName != null && Config.X509_VERIFICATION) {
+ return this.commonName;
+ } else if (this.systemName != null) {
+ return this.systemName;
+ } else if (this.serverName != null) {
+ return this.serverName;
+ } else if (this.presenceName != null) {
+ return this.presenceName;
+ } else if (jid.hasLocalpart()) {
+ return jid.getLocalpart();
+ } else {
+ return jid.getDomainpart();
+ }
+ }
+
+ @Override
+ public String getDisplayJid() {
+ if (jid != null) {
+ return jid.toString();
+ } else {
+ return null;
+ }
+ }
+
+ public String getProfilePhoto() {
+ return this.photoUri;
+ }
+
+ public Jid getJid() {
+ return jid;
+ }
+
+ @Override
+ public List<Tag> getTags(Context context) {
+ final ArrayList<Tag> tags = new ArrayList<>();
+ for (final String group : getGroups()) {
+ tags.add(new Tag(group, UIHelper.getColorForName(group)));
+ }
+ Presence.Status status = getShownStatus();
+ if (status != Presence.Status.OFFLINE) {
+ tags.add(UIHelper.getTagForStatus(context, status));
+ }
+ if (isBlocked()) {
+ tags.add(new Tag("blocked", 0xff2e2f3b));
+ }
+ return tags;
+ }
+
+ public boolean match(Context context, String needle) {
+ if (needle == null || needle.isEmpty()) {
+ return true;
+ }
+ needle = needle.toLowerCase(Locale.US).trim();
+ String[] parts = needle.split("\\s+");
+ if (parts.length > 1) {
+ for(int i = 0; i < parts.length; ++i) {
+ if (!match(context, parts[i])) {
+ return false;
+ }
+ }
+ return true;
+ } else {
+ return jid.toString().contains(needle) ||
+ getDisplayName().toLowerCase(Locale.US).contains(needle) ||
+ matchInTag(context, needle);
+ }
+ }
+
+ private boolean matchInTag(Context context, String needle) {
+ needle = needle.toLowerCase(Locale.US);
+ for (Tag tag : getTags(context)) {
+ if (tag.getName().toLowerCase(Locale.US).contains(needle)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public ContentValues getContentValues() {
+ synchronized (this.keys) {
+ final ContentValues values = new ContentValues();
+ values.put(ACCOUNT, accountUuid);
+ values.put(SYSTEMNAME, systemName);
+ values.put(SERVERNAME, serverName);
+ values.put(JID, jid.toString());
+ values.put(OPTIONS, subscription);
+ values.put(SYSTEMACCOUNT, systemAccount);
+ values.put(PHOTOURI, photoUri);
+ values.put(KEYS, keys.toString());
+ values.put(AVATAR, avatar == null ? null : avatar.getFilename());
+ values.put(LAST_PRESENCE, mLastPresence);
+ values.put(LAST_TIME, mLastseen);
+ values.put(GROUPS, groups.toString());
+ return values;
+ }
+ }
+
+ public int getSubscription() {
+ return this.subscription;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public void setAccount(Account account) {
+ this.account = account;
+ this.accountUuid = account.getUuid();
+ }
+
+ public Presences getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(final String resource, final Presence presence) {
+ this.presences.updatePresence(resource, presence);
+ }
+
+ public void removePresence(final String resource) {
+ this.presences.removePresence(resource);
+ }
+
+ public void clearPresences() {
+ this.presences.clearPresences();
+ this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+ }
+
+ public Presence.Status getShownStatus() {
+ return this.presences.getShownStatus();
+ }
+
+ public boolean setPhotoUri(String uri) {
+ if (uri != null && !uri.equals(this.photoUri)) {
+ this.photoUri = uri;
+ return true;
+ } else if (this.photoUri != null && uri == null) {
+ this.photoUri = null;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void setServerName(String serverName) {
+ this.serverName = serverName;
+ }
+
+ public void setSystemName(String systemName) {
+ this.systemName = systemName;
+ }
+
+ public void setPresenceName(String presenceName) {
+ this.presenceName = presenceName;
+ }
+
+ public String getSystemAccount() {
+ return systemAccount;
+ }
+
+ public void setSystemAccount(String account) {
+ this.systemAccount = account;
+ }
+
+ public List<String> getGroups() {
+ ArrayList<String> groups = new ArrayList<String>();
+ for (int i = 0; i < this.groups.length(); ++i) {
+ try {
+ groups.add(this.groups.getString(i));
+ } catch (final JSONException ignored) {
+ }
+ }
+ return groups;
+ }
+
+ public ArrayList<String> getOtrFingerprints() {
+ synchronized (this.keys) {
+ final ArrayList<String> fingerprints = new ArrayList<String>();
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ final JSONArray prints = this.keys.getJSONArray("otr_fingerprints");
+ for (int i = 0; i < prints.length(); ++i) {
+ final String print = prints.isNull(i) ? null : prints.getString(i);
+ if (print != null && !print.isEmpty()) {
+ fingerprints.add(prints.getString(i));
+ }
+ }
+ }
+ } catch (final JSONException ignored) {
+
+ }
+ return fingerprints;
+ }
+ }
+ public boolean addOtrFingerprint(String print) {
+ synchronized (this.keys) {
+ if (getOtrFingerprints().contains(print)) {
+ return false;
+ }
+ try {
+ JSONArray fingerprints;
+ if (!this.keys.has("otr_fingerprints")) {
+ fingerprints = new JSONArray();
+ } else {
+ fingerprints = this.keys.getJSONArray("otr_fingerprints");
+ }
+ fingerprints.put(print);
+ this.keys.put("otr_fingerprints", fingerprints);
+ return true;
+ } catch (final JSONException ignored) {
+ return false;
+ }
+ }
+ }
+
+ public long getPgpKeyId() {
+ synchronized (this.keys) {
+ if (this.keys.has("pgp_keyid")) {
+ try {
+ return this.keys.getLong("pgp_keyid");
+ } catch (JSONException e) {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+ }
+
+ public void setPgpKeyId(long keyId) {
+ synchronized (this.keys) {
+ try {
+ this.keys.put("pgp_keyid", keyId);
+ } catch (final JSONException ignored) {
+ }
+ }
+ }
+
+ public void setOption(int option) {
+ this.subscription |= 1 << option;
+ }
+
+ public void resetOption(int option) {
+ this.subscription &= ~(1 << option);
+ }
+
+ public boolean getOption(int option) {
+ return ((this.subscription & (1 << option)) != 0);
+ }
+
+ public boolean showInRoster() {
+ return (this.getOption(Contact.Options.IN_ROSTER) && (!this
+ .getOption(Contact.Options.DIRTY_DELETE)))
+ || (this.getOption(Contact.Options.DIRTY_PUSH));
+ }
+
+ public void parseSubscriptionFromElement(Element item) {
+ String ask = item.getAttribute("ask");
+ String subscription = item.getAttribute("subscription");
+
+ if (subscription != null) {
+ switch (subscription) {
+ case "to":
+ this.resetOption(Options.FROM);
+ this.setOption(Options.TO);
+ break;
+ case "from":
+ this.resetOption(Options.TO);
+ this.setOption(Options.FROM);
+ this.resetOption(Options.PREEMPTIVE_GRANT);
+ this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+ break;
+ case "both":
+ this.setOption(Options.TO);
+ this.setOption(Options.FROM);
+ this.resetOption(Options.PREEMPTIVE_GRANT);
+ this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST);
+ break;
+ case "none":
+ this.resetOption(Options.FROM);
+ this.resetOption(Options.TO);
+ break;
+ }
+ }
+
+ // do NOT override asking if pending push request
+ if (!this.getOption(Contact.Options.DIRTY_PUSH)) {
+ if ((ask != null) && (ask.equals("subscribe"))) {
+ this.setOption(Contact.Options.ASKING);
+ } else {
+ this.resetOption(Contact.Options.ASKING);
+ }
+ }
+ }
+
+ public void parseGroupsFromElement(Element item) {
+ this.groups = new JSONArray();
+ for (Element element : item.getChildren()) {
+ if (element.getName().equals("group") && element.getContent() != null) {
+ this.groups.put(element.getContent());
+ }
+ }
+ }
+
+ public Element asElement() {
+ final Element item = new Element("item");
+ item.setAttribute("jid", this.jid.toString());
+ if (this.serverName != null) {
+ item.setAttribute("name", this.serverName);
+ }
+ for (String group : getGroups()) {
+ item.addChild("group").setContent(group);
+ }
+ return item;
+ }
+
+ @Override
+ public int compareTo(final ListItem another) {
+ return this.getDisplayName().compareToIgnoreCase(
+ another.getDisplayName());
+ }
+
+ public Jid getServer() {
+ return getJid().toDomainJid();
+ }
+
+ public boolean setAvatar(Avatar avatar) {
+ if (this.avatar != null && this.avatar.equals(avatar)) {
+ return false;
+ } else {
+ if (this.avatar != null && this.avatar.origin == Avatar.Origin.PEP && avatar.origin == Avatar.Origin.VCARD) {
+ return false;
+ }
+ this.avatar = avatar;
+ return true;
+ }
+ }
+
+ public String getAvatar() {
+ return avatar == null ? null : avatar.getFilename();
+ }
+
+ public boolean deleteOtrFingerprint(String fingerprint) {
+ synchronized (this.keys) {
+ boolean success = false;
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ JSONArray newPrints = new JSONArray();
+ JSONArray oldPrints = this.keys
+ .getJSONArray("otr_fingerprints");
+ for (int i = 0; i < oldPrints.length(); ++i) {
+ if (!oldPrints.getString(i).equals(fingerprint)) {
+ newPrints.put(oldPrints.getString(i));
+ } else {
+ success = true;
+ }
+ }
+ this.keys.put("otr_fingerprints", newPrints);
+ }
+ return success;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+ }
+
+ public boolean trusted() {
+ return getOption(Options.FROM) && getOption(Options.TO);
+ }
+
+ @Override
+ public boolean isBlocked() {
+ return getAccount().isBlocked(this);
+ }
+
+ @Override
+ public boolean isDomainBlocked() {
+ return getAccount().isBlocked(this.getJid().toDomainJid());
+ }
+
+ @Override
+ public Jid getBlockedJid() {
+ if (isDomainBlocked()) {
+ return getJid().toDomainJid();
+ } else {
+ return getJid();
+ }
+ }
+
+ public boolean isSelf() {
+ return account.getJid().toBareJid().equals(getJid().toBareJid());
+ }
+
+ public void setCommonName(String cn) {
+ this.commonName = cn;
+ }
+
+ public void flagActive() {
+ this.mActive = true;
+ }
+
+ public void flagInactive() {
+ this.mActive = false;
+ }
+
+ public boolean isActive() {
+ return this.mActive;
+ }
+
+ public void setLastseen(long timestamp) {
+ this.mLastseen = Math.max(timestamp, mLastseen);
+ }
+
+ public long getLastseen() {
+ return this.mLastseen;
+ }
+
+ public void setLastPresence(String presence) {
+ this.mLastPresence = presence;
+ }
+
+ public String getLastPresence() {
+ return this.mLastPresence;
+ }
+
+ public final class Options {
+ public static final int TO = 0;
+ public static final int FROM = 1;
+ public static final int ASKING = 2;
+ public static final int PREEMPTIVE_GRANT = 3;
+ public static final int IN_ROSTER = 4;
+ public static final int PENDING_SUBSCRIPTION_REQUEST = 5;
+ public static final int DIRTY_PUSH = 6;
+ public static final int DIRTY_DELETE = 7;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Conversation.java b/src/main/java/de/pixart/messenger/entities/Conversation.java
new file mode 100644
index 000000000..11eff3f6e
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Conversation.java
@@ -0,0 +1,1007 @@
+package de.pixart.messenger.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Iterator;
+import java.util.List;
+
+import de.pixart.messenger.Config;
+import de.pixart.messenger.crypto.PgpDecryptionService;
+import de.pixart.messenger.crypto.axolotl.AxolotlService;
+import de.pixart.messenger.xmpp.chatstate.ChatState;
+import de.pixart.messenger.xmpp.jid.InvalidJidException;
+import de.pixart.messenger.xmpp.jid.Jid;
+
+
+public class Conversation extends AbstractEntity implements Blockable, Comparable<Conversation> {
+ public static final String TABLENAME = "conversations";
+
+ public static final int STATUS_AVAILABLE = 0;
+ public static final int STATUS_ARCHIVED = 1;
+ public static final int STATUS_DELETED = 2;
+
+ public static final int MODE_MULTI = 1;
+ public static final int MODE_SINGLE = 0;
+
+ public static final String NAME = "name";
+ public static final String ACCOUNT = "accountUuid";
+ public static final String CONTACT = "contactUuid";
+ public static final String CONTACTJID = "contactJid";
+ public static final String STATUS = "status";
+ public static final String CREATED = "created";
+ public static final String MODE = "mode";
+ public static final String ATTRIBUTES = "attributes";
+
+ public static final String ATTRIBUTE_NEXT_ENCRYPTION = "next_encryption";
+ public static final String ATTRIBUTE_MUC_PASSWORD = "muc_password";
+ public static final String ATTRIBUTE_MUTED_TILL = "muted_till";
+ public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify";
+ public static final String ATTRIBUTE_CRYPTO_TARGETS = "crypto_targets";
+
+ private String name;
+ private String contactUuid;
+ private String accountUuid;
+ private Jid contactJid;
+ private int status;
+ private long created;
+ private int mode;
+
+ private JSONObject attributes = new JSONObject();
+
+ private Jid nextCounterpart;
+
+ protected final ArrayList<Message> messages = new ArrayList<>();
+ protected Account account = null;
+
+ private transient SessionImpl otrSession;
+
+ private transient String otrFingerprint = null;
+ private Smp mSmp = new Smp();
+
+ private String nextMessage;
+
+ private transient MucOptions mucOptions = null;
+
+ private byte[] symmetricKey;
+
+ private Bookmark bookmark;
+
+ private boolean messagesLeftOnServer = true;
+ private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE;
+ private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE;
+ private String mLastReceivedOtrMessageId = null;
+ private String mFirstMamReference = null;
+ private Message correctingMessage;
+
+ public boolean hasMessagesLeftOnServer() {
+ return messagesLeftOnServer;
+ }
+
+ public void setHasMessagesLeftOnServer(boolean value) {
+ this.messagesLeftOnServer = value;
+ }
+
+
+ public Message getFirstUnreadMessage() {
+ Message first = null;
+ synchronized (this.messages) {
+ for (int i = messages.size() - 1; i >= 0; --i) {
+ if (messages.get(i).isRead()) {
+ return first;
+ } else {
+ first = messages.get(i);
+ }
+ }
+ }
+ return first;
+ }
+
+ public Message findUnsentMessageWithUuid(String uuid) {
+ synchronized(this.messages) {
+ for (final Message message : this.messages) {
+ final int s = message.getStatus();
+ if ((s == Message.STATUS_UNSEND || s == Message.STATUS_WAITING) && message.getUuid().equals(uuid)) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void findWaitingMessages(OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for(Message message : this.messages) {
+ if (message.getStatus() == Message.STATUS_WAITING) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public void findUnreadMessages(OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for(Message message : this.messages) {
+ if (!message.isRead()) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public void findMessagesWithFiles(final OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for (final Message message : this.messages) {
+ if ((message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE)
+ && message.getEncryption() != Message.ENCRYPTION_PGP) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public Message findMessageWithFileAndUuid(final String uuid) {
+ synchronized (this.messages) {
+ for (final Message message : this.messages) {
+ if (message.getUuid().equals(uuid)
+ && message.getEncryption() != Message.ENCRYPTION_PGP
+ && (message.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE || message.treatAsDownloadable() != Message.Decision.NEVER)) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void clearMessages() {
+ synchronized (this.messages) {
+ this.messages.clear();
+ }
+ }
+
+ public boolean setIncomingChatState(ChatState state) {
+ if (this.mIncomingChatState == state) {
+ return false;
+ }
+ this.mIncomingChatState = state;
+ return true;
+ }
+
+ public ChatState getIncomingChatState() {
+ return this.mIncomingChatState;
+ }
+
+ public boolean setOutgoingChatState(ChatState state) {
+ if (mode == MODE_MULTI) {
+ return false;
+ }
+ if (this.mOutgoingChatState != state) {
+ this.mOutgoingChatState = state;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public ChatState getOutgoingChatState() {
+ return this.mOutgoingChatState;
+ }
+
+ public void trim() {
+ synchronized (this.messages) {
+ final int size = messages.size();
+ final int maxsize = Config.PAGE_SIZE * Config.MAX_NUM_PAGES;
+ if (size > maxsize) {
+ List<Message> discards = this.messages.subList(0, size - maxsize);
+ final PgpDecryptionService pgpDecryptionService = account.getPgpDecryptionService();
+ if (pgpDecryptionService != null) {
+ pgpDecryptionService.discard(discards);
+ }
+ discards.clear();
+ untieMessages();
+ }
+ }
+ }
+
+ public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for (Message message : this.messages) {
+ if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING)
+ && (message.getEncryption() == encryptionType)) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public void findUnsentTextMessages(OnMessageFound onMessageFound) {
+ synchronized (this.messages) {
+ for (Message message : this.messages) {
+ if (message.getType() != Message.TYPE_IMAGE
+ && message.getStatus() == Message.STATUS_UNSEND) {
+ onMessageFound.onMessageFound(message);
+ }
+ }
+ }
+ }
+
+ public Message findSentMessageWithUuidOrRemoteId(String id) {
+ synchronized (this.messages) {
+ for (Message message : this.messages) {
+ if (id.equals(message.getUuid())
+ || (message.getStatus() >= Message.STATUS_SEND
+ && id.equals(message.getRemoteMsgId()))) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
+ public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() - 1; i >= 0; --i) {
+ Message message = messages.get(i);
+ if (counterpart.equals(message.getCounterpart())
+ && ((message.getStatus() == Message.STATUS_RECEIVED) == received)
+ && (carbon == message.isCarbon() || received) ) {
+ if (id.equals(message.getRemoteMsgId())) {
+ return message;
+ } else {
+ return null;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ public Message findSentMessageWithUuid(String id) {
+ synchronized (this.messages) {
+ for (Message message : this.messages) {
+ if (id.equals(message.getUuid())) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+
+ public void populateWithMessages(final List<Message> messages) {
+ synchronized (this.messages) {
+ messages.clear();
+ messages.addAll(this.messages);
+ }
+ for(Iterator<Message> iterator = messages.iterator(); iterator.hasNext();) {
+ if (iterator.next().wasMergedIntoPrevious()) {
+ iterator.remove();
+ }
+ }
+ }
+
+ @Override
+ public boolean isBlocked() {
+ return getContact().isBlocked();
+ }
+
+ @Override
+ public boolean isDomainBlocked() {
+ return getContact().isDomainBlocked();
+ }
+
+ @Override
+ public Jid getBlockedJid() {
+ return getContact().getBlockedJid();
+ }
+
+ public String getLastReceivedOtrMessageId() {
+ return this.mLastReceivedOtrMessageId;
+ }
+
+ public void setLastReceivedOtrMessageId(String id) {
+ this.mLastReceivedOtrMessageId = id;
+ }
+
+ public int countMessages() {
+ synchronized (this.messages) {
+ return this.messages.size();
+ }
+ }
+
+ public void setFirstMamReference(String reference) {
+ this.mFirstMamReference = reference;
+ }
+
+ public String getFirstMamReference() {
+ return this.mFirstMamReference;
+ }
+
+ public void setLastClearHistory(long time) {
+ setAttribute("last_clear_history",String.valueOf(time));
+ }
+
+ public long getLastClearHistory() {
+ return getLongAttribute("last_clear_history", 0);
+ }
+
+ public List<Jid> getAcceptedCryptoTargets() {
+ if (mode == MODE_SINGLE) {
+ return Arrays.asList(getJid().toBareJid());
+ } else {
+ return getJidListAttribute(ATTRIBUTE_CRYPTO_TARGETS);
+ }
+ }
+
+ public void setAcceptedCryptoTargets(List<Jid> acceptedTargets) {
+ setAttribute(ATTRIBUTE_CRYPTO_TARGETS, acceptedTargets);
+ }
+
+ public void setCorrectingMessage(Message correctingMessage) {
+ this.correctingMessage = correctingMessage;
+ }
+
+ public Message getCorrectingMessage() {
+ return this.correctingMessage;
+ }
+
+ public boolean withSelf() {
+ return getContact().isSelf();
+ }
+
+ @Override
+ public int compareTo(Conversation another) {
+ final Message left = getLatestMessage();
+ final Message right = another.getLatestMessage();
+ if (left.getTimeSent() > right.getTimeSent()) {
+ return -1;
+ } else if (left.getTimeSent() < right.getTimeSent()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+
+ public interface OnMessageFound {
+ void onMessageFound(final Message message);
+ }
+
+ public Conversation(final String name, final Account account, final Jid contactJid,
+ final int mode) {
+ this(java.util.UUID.randomUUID().toString(), name, null, account
+ .getUuid(), contactJid, System.currentTimeMillis(),
+ STATUS_AVAILABLE, mode, "");
+ this.account = account;
+ }
+
+ public Conversation(final String uuid, final String name, final String contactUuid,
+ final String accountUuid, final Jid contactJid, final long created, final int status,
+ final int mode, final String attributes) {
+ this.uuid = uuid;
+ this.name = name;
+ this.contactUuid = contactUuid;
+ this.accountUuid = accountUuid;
+ this.contactJid = contactJid;
+ this.created = created;
+ this.status = status;
+ this.mode = mode;
+ try {
+ this.attributes = new JSONObject(attributes == null ? "" : attributes);
+ } catch (JSONException e) {
+ this.attributes = new JSONObject();
+ }
+ }
+
+ public boolean isRead() {
+ return (this.messages.size() == 0) || this.messages.get(this.messages.size() - 1).isRead();
+ }
+
+ public List<Message> markRead() {
+ final List<Message> unread = new ArrayList<>();
+ synchronized (this.messages) {
+ for(Message message : this.messages) {
+ if (!message.isRead()) {
+ message.markRead();
+ unread.add(message);
+ }
+ }
+ }
+ return unread;
+ }
+
+ public Message getLatestMarkableMessage() {
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).getStatus() <= Message.STATUS_RECEIVED
+ && this.messages.get(i).markable) {
+ if (this.messages.get(i).isRead()) {
+ return null;
+ } else {
+ return this.messages.get(i);
+ }
+ }
+ }
+ return null;
+ }
+
+ public Message getLatestMessage() {
+ if (this.messages.size() == 0) {
+ Message message = new Message(this, "", Message.ENCRYPTION_NONE);
+ message.setTime(getCreated());
+ return message;
+ } else {
+ Message message = this.messages.get(this.messages.size() - 1);
+ message.setConversation(this);
+ return message;
+ }
+ }
+
+ public String getName() {
+ if (getMode() == MODE_MULTI) {
+ if (getMucOptions().getSubject() != null) {
+ return getMucOptions().getSubject();
+ } else if (bookmark != null
+ && bookmark.getBookmarkName() != null
+ && !bookmark.getBookmarkName().trim().isEmpty()) {
+ return bookmark.getBookmarkName().trim();
+ } else {
+ String generatedName = getMucOptions().createNameFromParticipants();
+ if (generatedName != null) {
+ return generatedName;
+ } else {
+ return getJid().getLocalpart();
+ }
+ }
+ } else {
+ return this.getContact().getDisplayName();
+ }
+ }
+
+ public String getParticipants() {
+ if (getMode() == MODE_MULTI) {
+ String generatedName = getMucOptions().createNameFromParticipants();
+ if (generatedName != null) {
+ return generatedName;
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ public String getAccountUuid() {
+ return this.accountUuid;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Contact getContact() {
+ return this.account.getRoster().getContact(this.contactJid);
+ }
+
+ public void setAccount(final Account account) {
+ this.account = account;
+ }
+
+ @Override
+ public Jid getJid() {
+ return this.contactJid;
+ }
+
+ public int getStatus() {
+ return this.status;
+ }
+
+ public long getCreated() {
+ return this.created;
+ }
+
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(NAME, name);
+ values.put(CONTACT, contactUuid);
+ values.put(ACCOUNT, accountUuid);
+ values.put(CONTACTJID, contactJid.toString());
+ values.put(CREATED, created);
+ values.put(STATUS, status);
+ values.put(MODE, mode);
+ values.put(ATTRIBUTES, attributes.toString());
+ return values;
+ }
+
+ public static Conversation fromCursor(Cursor cursor) {
+ Jid jid;
+ try {
+ jid = Jid.fromString(cursor.getString(cursor.getColumnIndex(CONTACTJID)), true);
+ } catch (final InvalidJidException e) {
+ // Borked DB..
+ jid = null;
+ }
+ return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(NAME)),
+ cursor.getString(cursor.getColumnIndex(CONTACT)),
+ cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ jid,
+ cursor.getLong(cursor.getColumnIndex(CREATED)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)),
+ cursor.getInt(cursor.getColumnIndex(MODE)),
+ cursor.getString(cursor.getColumnIndex(ATTRIBUTES)));
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public int getMode() {
+ return this.mode;
+ }
+
+ public void setMode(int mode) {
+ this.mode = mode;
+ }
+
+ public SessionImpl startOtrSession(String presence, boolean sendStart) {
+ if (this.otrSession != null) {
+ return this.otrSession;
+ } else {
+ final SessionID sessionId = new SessionID(this.getJid().toBareJid().toString(),
+ presence,
+ "xmpp");
+ this.otrSession = new SessionImpl(sessionId, getAccount().getOtrService());
+ try {
+ if (sendStart) {
+ this.otrSession.startSession();
+ return this.otrSession;
+ }
+ return this.otrSession;
+ } catch (OtrException e) {
+ return null;
+ }
+ }
+
+ }
+
+ public SessionImpl getOtrSession() {
+ return this.otrSession;
+ }
+
+ public void resetOtrSession() {
+ this.otrFingerprint = null;
+ this.otrSession = null;
+ this.mSmp.hint = null;
+ this.mSmp.secret = null;
+ this.mSmp.status = Smp.STATUS_NONE;
+ }
+
+ public Smp smp() {
+ return mSmp;
+ }
+
+ public boolean startOtrIfNeeded() {
+ if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) {
+ try {
+ this.otrSession.startSession();
+ return true;
+ } catch (OtrException e) {
+ this.resetOtrSession();
+ return false;
+ }
+ } else {
+ return true;
+ }
+ }
+
+ public boolean endOtrIfNeeded() {
+ if (this.otrSession != null) {
+ if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
+ try {
+ this.otrSession.endSession();
+ this.resetOtrSession();
+ return true;
+ } catch (OtrException e) {
+ this.resetOtrSession();
+ return false;
+ }
+ } else {
+ this.resetOtrSession();
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public boolean hasValidOtrSession() {
+ return this.otrSession != null;
+ }
+
+ public synchronized String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ if (getOtrSession() == null || getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) {
+ return null;
+ }
+ DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
+ this.otrFingerprint = getAccount().getOtrService().getFingerprint(remotePubKey);
+ } catch (final OtrCryptoException | UnsupportedOperationException ignored) {
+ return null;
+ }
+ }
+ return this.otrFingerprint;
+ }
+
+ public boolean verifyOtrFingerprint() {
+ final String fingerprint = getOtrFingerprint();
+ if (fingerprint != null) {
+ getContact().addOtrFingerprint(fingerprint);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean isOtrFingerprintVerified() {
+ return getContact().getOtrFingerprints().contains(getOtrFingerprint());
+ }
+
+ /**
+ * short for is Private and Non-anonymous
+ */
+ private boolean isPnNA() {
+ return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous());
+ }
+
+ public synchronized MucOptions getMucOptions() {
+ if (this.mucOptions == null) {
+ this.mucOptions = new MucOptions(this);
+ }
+ return this.mucOptions;
+ }
+
+ public void resetMucOptions() {
+ this.mucOptions = null;
+ }
+
+ public void setContactJid(final Jid jid) {
+ this.contactJid = jid;
+ }
+
+ public void setNextCounterpart(Jid jid) {
+ this.nextCounterpart = jid;
+ }
+
+ public Jid getNextCounterpart() {
+ return this.nextCounterpart;
+ }
+
+ private int getMostRecentlyUsedIncomingEncryption() {
+ synchronized (this.messages) {
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ final Message m = this.messages.get(i);
+ if (m.getStatus() == Message.STATUS_RECEIVED) {
+ final int e = m.getEncryption();
+ if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) {
+ return Message.ENCRYPTION_PGP;
+ } else {
+ return e;
+ }
+ }
+ }
+ }
+ return Message.ENCRYPTION_NONE;
+ }
+
+ public int getNextEncryption() {
+ final AxolotlService axolotlService = getAccount().getAxolotlService();
+ int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1);
+ if (next == -1) {
+ if (Config.supportOmemo()
+ && axolotlService != null
+ && mode == MODE_SINGLE
+ && axolotlService.isConversationAxolotlCapable(this)
+ && getAccount().getSelfContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)
+ && getContact().getPresences().allOrNonSupport(AxolotlService.PEP_DEVICE_LIST_NOTIFY)
+ && Config.AlwayUseOMEMO){
+ return Message.ENCRYPTION_AXOLOTL;
+ } else {
+ next = this.getMostRecentlyUsedIncomingEncryption();
+ }
+ }
+
+ if (!Config.supportUnencrypted() && next <= 0) {
+ if (Config.supportOmemo()
+ && ((axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) || !Config.multipleEncryptionChoices())) {
+ return Message.ENCRYPTION_AXOLOTL;
+ } else if (Config.supportOtr() && mode == MODE_SINGLE) {
+ return Message.ENCRYPTION_OTR;
+ } else if (Config.supportOpenPgp()
+ && (mode == MODE_SINGLE) || !Config.multipleEncryptionChoices()) {
+ return Message.ENCRYPTION_PGP;
+ }
+ } else if (next == Message.ENCRYPTION_AXOLOTL
+ && (!Config.supportOmemo() || axolotlService == null || !axolotlService.isConversationAxolotlCapable(this))) {
+ next = Message.ENCRYPTION_NONE;
+ }
+ return next;
+ }
+
+ public void setNextEncryption(int encryption) {
+ this.setAttribute(ATTRIBUTE_NEXT_ENCRYPTION, String.valueOf(encryption));
+ }
+
+ public String getNextMessage() {
+ if (this.nextMessage == null) {
+ return "";
+ } else {
+ return this.nextMessage;
+ }
+ }
+
+ public boolean smpRequested() {
+ return smp().status == Smp.STATUS_CONTACT_REQUESTED;
+ }
+
+ public void setNextMessage(String message) {
+ this.nextMessage = message;
+ }
+
+ public void setSymmetricKey(byte[] key) {
+ this.symmetricKey = key;
+ }
+
+ public byte[] getSymmetricKey() {
+ return this.symmetricKey;
+ }
+
+ public void setBookmark(Bookmark bookmark) {
+ this.bookmark = bookmark;
+ this.bookmark.setConversation(this);
+ }
+
+ public void deregisterWithBookmark() {
+ if (this.bookmark != null) {
+ this.bookmark.setConversation(null);
+ }
+ }
+
+ public Bookmark getBookmark() {
+ return this.bookmark;
+ }
+
+ public boolean hasDuplicateMessage(Message message) {
+ synchronized (this.messages) {
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).similar(message)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public Message findSentMessageWithBody(String body) {
+ synchronized (this.messages) {
+ for (int i = this.messages.size() - 1; i >= 0; --i) {
+ Message message = this.messages.get(i);
+ if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) {
+ String otherBody;
+ if (message.hasFileOnRemoteHost()) {
+ otherBody = message.getFileParams().url.toString();
+ } else {
+ otherBody = message.body;
+ }
+ if (otherBody != null && otherBody.equals(body)) {
+ return message;
+ }
+ }
+ }
+ return null;
+ }
+ }
+
+ public long getLastMessageTransmitted() {
+ long last_clear = getLastClearHistory();
+ if (last_clear != 0) {
+ return last_clear;
+ }
+ synchronized (this.messages) {
+ for(int i = this.messages.size() - 1; i >= 0; --i) {
+ Message message = this.messages.get(i);
+ if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) {
+ return message.getTimeSent();
+ }
+ }
+ }
+ return 0;
+ }
+
+ public void setMutedTill(long value) {
+ this.setAttribute(ATTRIBUTE_MUTED_TILL, String.valueOf(value));
+ }
+
+ public boolean isMuted() {
+ return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0);
+ }
+
+ public boolean alwaysNotify() {
+ return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA());
+ }
+
+ public boolean setAttribute(String key, String value) {
+ synchronized (this.attributes) {
+ try {
+ this.attributes.put(key, value);
+ return true;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+ }
+
+ public boolean setAttribute(String key, List<Jid> jids) {
+ JSONArray array = new JSONArray();
+ for(Jid jid : jids) {
+ array.put(jid.toBareJid().toString());
+ }
+ synchronized (this.attributes) {
+ try {
+ this.attributes.put(key, array);
+ return true;
+ } catch (JSONException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+ }
+
+ public String getAttribute(String key) {
+ synchronized (this.attributes) {
+ try {
+ return this.attributes.getString(key);
+ } catch (JSONException e) {
+ return null;
+ }
+ }
+ }
+
+ public List<Jid> getJidListAttribute(String key) {
+ ArrayList<Jid> list = new ArrayList<>();
+ synchronized (this.attributes) {
+ try {
+ JSONArray array = this.attributes.getJSONArray(key);
+ for (int i = 0; i < array.length(); ++i) {
+ try {
+ list.add(Jid.fromString(array.getString(i)));
+ } catch (InvalidJidException e) {
+ //ignored
+ }
+ }
+ } catch (JSONException e) {
+ //ignored
+ }
+ }
+ return list;
+ }
+
+ public int getIntAttribute(String key, int defaultValue) {
+ String value = this.getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+ }
+
+ public long getLongAttribute(String key, long defaultValue) {
+ String value = this.getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ try {
+ return Long.parseLong(value);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+ }
+
+ public boolean getBooleanAttribute(String key, boolean defaultValue) {
+ String value = this.getAttribute(key);
+ if (value == null) {
+ return defaultValue;
+ } else {
+ return Boolean.parseBoolean(value);
+ }
+ }
+
+ public void add(Message message) {
+ message.setConversation(this);
+ synchronized (this.messages) {
+ this.messages.add(message);
+ }
+ }
+
+ public void prepend(Message message) {
+ message.setConversation(this);
+ synchronized (this.messages) {
+ this.messages.add(0,message);
+ }
+ }
+
+ public void addAll(int index, List<Message> messages) {
+ synchronized (this.messages) {
+ this.messages.addAll(index, messages);
+ }
+ account.getPgpDecryptionService().decrypt(messages);
+ }
+
+ public void sort() {
+ synchronized (this.messages) {
+ Collections.sort(this.messages, new Comparator<Message>() {
+ @Override
+ public int compare(Message left, Message right) {
+ if (left.getTimeSent() < right.getTimeSent()) {
+ return -1;
+ } else if (left.getTimeSent() > right.getTimeSent()) {
+ return 1;
+ } else {
+ return 0;
+ }
+ }
+ });
+ untieMessages();
+ }
+ }
+
+ private void untieMessages() {
+ for(Message message : this.messages) {
+ message.untie();
+ }
+ }
+
+ public int unreadCount() {
+ synchronized (this.messages) {
+ int count = 0;
+ for(int i = this.messages.size() - 1; i >= 0; --i) {
+ if (this.messages.get(i).isRead()) {
+ return count;
+ }
+ ++count;
+ }
+ return count;
+ }
+ }
+
+ public class Smp {
+ public static final int STATUS_NONE = 0;
+ public static final int STATUS_CONTACT_REQUESTED = 1;
+ public static final int STATUS_WE_REQUESTED = 2;
+ public static final int STATUS_FAILED = 3;
+ public static final int STATUS_VERIFIED = 4;
+
+ public String secret = null;
+ public String hint = null;
+ public int status = 0;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/DownloadableFile.java b/src/main/java/de/pixart/messenger/entities/DownloadableFile.java
new file mode 100644
index 000000000..1a6e88881
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/DownloadableFile.java
@@ -0,0 +1,83 @@
+package de.pixart.messenger.entities;
+
+import java.io.File;
+
+import de.pixart.messenger.utils.MimeUtils;
+
+public class DownloadableFile extends File {
+
+ private static final long serialVersionUID = 2247012619505115863L;
+
+ private long expectedSize = 0;
+ private String sha1sum;
+ private byte[] aeskey;
+
+ private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+ 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf };
+
+ public DownloadableFile(String path) {
+ super(path);
+ }
+
+ public long getSize() {
+ return super.length();
+ }
+
+ public long getExpectedSize() {
+ return this.expectedSize;
+ }
+
+ public String getMimeType() {
+ String path = this.getAbsolutePath();
+ int start = path.lastIndexOf('.') + 1;
+ if (start < path.length()) {
+ String mime = MimeUtils.guessMimeTypeFromExtension(path.substring(start));
+ return mime == null ? "" : mime;
+ } else {
+ return "";
+ }
+ }
+
+ public void setExpectedSize(long size) {
+ this.expectedSize = size;
+ }
+
+ public String getSha1Sum() {
+ return this.sha1sum;
+ }
+
+ public void setSha1Sum(String sum) {
+ this.sha1sum = sum;
+ }
+
+ public void setKeyAndIv(byte[] keyIvCombo) {
+ if (keyIvCombo.length == 48) {
+ this.aeskey = new byte[32];
+ this.iv = new byte[16];
+ System.arraycopy(keyIvCombo, 0, this.iv, 0, 16);
+ System.arraycopy(keyIvCombo, 16, this.aeskey, 0, 32);
+ } else if (keyIvCombo.length >= 32) {
+ this.aeskey = new byte[32];
+ System.arraycopy(keyIvCombo, 0, aeskey, 0, 32);
+ } else if (keyIvCombo.length >= 16) {
+ this.aeskey = new byte[16];
+ System.arraycopy(keyIvCombo, 0, this.aeskey, 0, 16);
+ }
+ }
+
+ public void setKey(byte[] key) {
+ this.aeskey = key;
+ }
+
+ public void setIv(byte[] iv) {
+ this.iv = iv;
+ }
+
+ public byte[] getKey() {
+ return this.aeskey;
+ }
+
+ public byte[] getIv() {
+ return this.iv;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/ListItem.java b/src/main/java/de/pixart/messenger/entities/ListItem.java
new file mode 100644
index 000000000..cb0644499
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/ListItem.java
@@ -0,0 +1,37 @@
+package de.pixart.messenger.entities;
+
+import android.content.Context;
+
+import java.util.List;
+
+import de.pixart.messenger.xmpp.jid.Jid;
+
+public interface ListItem extends Comparable<ListItem> {
+ String getDisplayName();
+
+ String getDisplayJid();
+
+ Jid getJid();
+
+ List<Tag> getTags(Context context);
+
+ final class Tag {
+ private final String name;
+ private final int color;
+
+ public Tag(final String name, final int color) {
+ this.name = name;
+ this.color = color;
+ }
+
+ public int getColor() {
+ return this.color;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+ }
+
+ boolean match(Context context, final String needle);
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Message.java b/src/main/java/de/pixart/messenger/entities/Message.java
new file mode 100644
index 000000000..3145cdccf
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Message.java
@@ -0,0 +1,831 @@
+package de.pixart.messenger.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import de.pixart.messenger.Config;
+import de.pixart.messenger.crypto.axolotl.XmppAxolotlSession;
+import de.pixart.messenger.utils.GeoHelper;
+import de.pixart.messenger.utils.MimeUtils;
+import de.pixart.messenger.utils.UIHelper;
+import de.pixart.messenger.xmpp.jid.InvalidJidException;
+import de.pixart.messenger.xmpp.jid.Jid;
+
+public class Message extends AbstractEntity {
+
+ public static final String TABLENAME = "messages";
+
+ public static final String MERGE_SEPARATOR = "\n\u200B\n";
+
+ public static final int STATUS_RECEIVED = 0;
+ public static final int STATUS_UNSEND = 1;
+ public static final int STATUS_SEND = 2;
+ public static final int STATUS_SEND_FAILED = 3;
+ public static final int STATUS_WAITING = 5;
+ public static final int STATUS_OFFERED = 6;
+ public static final int STATUS_SEND_RECEIVED = 7;
+ public static final int STATUS_SEND_DISPLAYED = 8;
+
+ public static final int ENCRYPTION_NONE = 0;
+ public static final int ENCRYPTION_PGP = 1;
+ public static final int ENCRYPTION_OTR = 2;
+ public static final int ENCRYPTION_DECRYPTED = 3;
+ public static final int ENCRYPTION_DECRYPTION_FAILED = 4;
+ public static final int ENCRYPTION_AXOLOTL = 5;
+
+ public static final int TYPE_TEXT = 0;
+ public static final int TYPE_IMAGE = 1;
+ public static final int TYPE_FILE = 2;
+ public static final int TYPE_STATUS = 3;
+ public static final int TYPE_PRIVATE = 4;
+
+ public static final String CONVERSATION = "conversationUuid";
+ public static final String COUNTERPART = "counterpart";
+ public static final String TRUE_COUNTERPART = "trueCounterpart";
+ public static final String BODY = "body";
+ public static final String TIME_SENT = "timeSent";
+ public static final String ENCRYPTION = "encryption";
+ public static final String STATUS = "status";
+ public static final String TYPE = "type";
+ public static final String CARBON = "carbon";
+ public static final String OOB = "oob";
+ public static final String EDITED = "edited";
+ public static final String REMOTE_MSG_ID = "remoteMsgId";
+ public static final String SERVER_MSG_ID = "serverMsgId";
+ public static final String RELATIVE_FILE_PATH = "relativeFilePath";
+ public static final String FINGERPRINT = "axolotl_fingerprint";
+ public static final String READ = "read";
+ public static final String ME_COMMAND = "/me ";
+
+
+ public boolean markable = false;
+ protected String conversationUuid;
+ protected Jid counterpart;
+ protected Jid trueCounterpart;
+ protected String body;
+ protected String encryptedBody;
+ protected long timeSent;
+ protected int encryption;
+ protected int status;
+ protected int type;
+ protected boolean carbon = false;
+ protected boolean oob = false;
+ protected String edited = null;
+ protected String relativeFilePath;
+ protected boolean read = true;
+ protected String remoteMsgId = null;
+ protected String serverMsgId = null;
+ protected Conversation conversation = null;
+ protected Transferable transferable = null;
+ private Message mNextMessage = null;
+ private Message mPreviousMessage = null;
+ private String axolotlFingerprint = null;
+
+ private Message() {
+
+ }
+
+ public Message(Conversation conversation, String body, int encryption) {
+ this(conversation, body, encryption, STATUS_UNSEND);
+ }
+
+ public Message(Conversation conversation, String body, int encryption, int status) {
+ this(java.util.UUID.randomUUID().toString(),
+ conversation.getUuid(),
+ conversation.getJid() == null ? null : conversation.getJid().toBareJid(),
+ null,
+ body,
+ System.currentTimeMillis(),
+ encryption,
+ status,
+ TYPE_TEXT,
+ false,
+ null,
+ null,
+ null,
+ null,
+ true,
+ null,
+ false);
+ this.conversation = conversation;
+ }
+
+ private Message(final String uuid, final String conversationUUid, final Jid counterpart,
+ final Jid trueCounterpart, final String body, final long timeSent,
+ final int encryption, final int status, final int type, final boolean carbon,
+ final String remoteMsgId, final String relativeFilePath,
+ final String serverMsgId, final String fingerprint, final boolean read,
+ final String edited, final boolean oob) {
+ this.uuid = uuid;
+ this.conversationUuid = conversationUUid;
+ this.counterpart = counterpart;
+ this.trueCounterpart = trueCounterpart;
+ this.body = body;
+ this.timeSent = timeSent;
+ this.encryption = encryption;
+ this.status = status;
+ this.type = type;
+ this.carbon = carbon;
+ this.remoteMsgId = remoteMsgId;
+ this.relativeFilePath = relativeFilePath;
+ this.serverMsgId = serverMsgId;
+ this.axolotlFingerprint = fingerprint;
+ this.read = read;
+ this.edited = edited;
+ this.oob = oob;
+ }
+
+ public static Message fromCursor(Cursor cursor) {
+ Jid jid;
+ try {
+ String value = cursor.getString(cursor.getColumnIndex(COUNTERPART));
+ if (value != null) {
+ jid = Jid.fromString(value, true);
+ } else {
+ jid = null;
+ }
+ } catch (InvalidJidException e) {
+ jid = null;
+ }
+ Jid trueCounterpart;
+ try {
+ String value = cursor.getString(cursor.getColumnIndex(TRUE_COUNTERPART));
+ if (value != null) {
+ trueCounterpart = Jid.fromString(value, true);
+ } else {
+ trueCounterpart = null;
+ }
+ } catch (InvalidJidException e) {
+ trueCounterpart = null;
+ }
+ return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(CONVERSATION)),
+ jid,
+ trueCounterpart,
+ cursor.getString(cursor.getColumnIndex(BODY)),
+ cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
+ cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)),
+ cursor.getInt(cursor.getColumnIndex(TYPE)),
+ cursor.getInt(cursor.getColumnIndex(CARBON)) > 0,
+ cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)),
+ cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)),
+ cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)),
+ cursor.getString(cursor.getColumnIndex(FINGERPRINT)),
+ cursor.getInt(cursor.getColumnIndex(READ)) > 0,
+ cursor.getString(cursor.getColumnIndex(EDITED)),
+ cursor.getInt(cursor.getColumnIndex(OOB)) > 0);
+ }
+
+ public static Message createStatusMessage(Conversation conversation, String body) {
+ final Message message = new Message();
+ message.setType(Message.TYPE_STATUS);
+ message.setConversation(conversation);
+ message.setBody(body);
+ return message;
+ }
+
+ public static Message createLoadMoreMessage(Conversation conversation) {
+ final Message message = new Message();
+ message.setType(Message.TYPE_STATUS);
+ message.setConversation(conversation);
+ message.setBody("LOAD_MORE");
+ return message;
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(CONVERSATION, conversationUuid);
+ if (counterpart == null) {
+ values.putNull(COUNTERPART);
+ } else {
+ values.put(COUNTERPART, counterpart.toString());
+ }
+ if (trueCounterpart == null) {
+ values.putNull(TRUE_COUNTERPART);
+ } else {
+ values.put(TRUE_COUNTERPART, trueCounterpart.toString());
+ }
+ values.put(BODY, body);
+ values.put(TIME_SENT, timeSent);
+ values.put(ENCRYPTION, encryption);
+ values.put(STATUS, status);
+ values.put(TYPE, type);
+ values.put(CARBON, carbon ? 1 : 0);
+ values.put(REMOTE_MSG_ID, remoteMsgId);
+ values.put(RELATIVE_FILE_PATH, relativeFilePath);
+ values.put(SERVER_MSG_ID, serverMsgId);
+ values.put(FINGERPRINT, axolotlFingerprint);
+ values.put(READ,read ? 1 : 0);
+ values.put(EDITED, edited);
+ values.put(OOB, oob ? 1 : 0);
+ return values;
+ }
+
+ public String getConversationUuid() {
+ return conversationUuid;
+ }
+
+ public Conversation getConversation() {
+ return this.conversation;
+ }
+
+ public void setConversation(Conversation conv) {
+ this.conversation = conv;
+ }
+
+ public Jid getCounterpart() {
+ return counterpart;
+ }
+
+ public void setCounterpart(final Jid counterpart) {
+ this.counterpart = counterpart;
+ }
+
+ public Contact getContact() {
+ if (this.conversation.getMode() == Conversation.MODE_SINGLE) {
+ return this.conversation.getContact();
+ } else {
+ if (this.trueCounterpart == null) {
+ return null;
+ } else {
+ return this.conversation.getAccount().getRoster()
+ .getContactFromRoster(this.trueCounterpart);
+ }
+ }
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+
+ public long getTimeSent() {
+ return timeSent;
+ }
+
+ public int getEncryption() {
+ return encryption;
+ }
+
+ public void setEncryption(int encryption) {
+ this.encryption = encryption;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public String getRelativeFilePath() {
+ return this.relativeFilePath;
+ }
+
+ public void setRelativeFilePath(String path) {
+ this.relativeFilePath = path;
+ }
+
+ public String getRemoteMsgId() {
+ return this.remoteMsgId;
+ }
+
+ public void setRemoteMsgId(String id) {
+ this.remoteMsgId = id;
+ }
+
+ public String getServerMsgId() {
+ return this.serverMsgId;
+ }
+
+ public void setServerMsgId(String id) {
+ this.serverMsgId = id;
+ }
+
+ public boolean isRead() {
+ return this.read;
+ }
+
+ public void markRead() {
+ this.read = true;
+ }
+
+ public void markUnread() {
+ this.read = false;
+ }
+
+ public void setTime(long time) {
+ this.timeSent = time;
+ }
+
+ public String getEncryptedBody() {
+ return this.encryptedBody;
+ }
+
+ public void setEncryptedBody(String body) {
+ this.encryptedBody = body;
+ }
+
+ public int getType() {
+ return this.type;
+ }
+
+ public void setType(int type) {
+ this.type = type;
+ }
+
+ public boolean isCarbon() {
+ return carbon;
+ }
+
+ public void setCarbon(boolean carbon) {
+ this.carbon = carbon;
+ }
+
+ public void setEdited(String edited) {
+ this.edited = edited;
+ }
+
+ public boolean edited() {
+ return this.edited != null;
+ }
+
+ public void setTrueCounterpart(Jid trueCounterpart) {
+ this.trueCounterpart = trueCounterpart;
+ }
+
+ public Jid getTrueCounterpart() {
+ return this.trueCounterpart;
+ }
+
+ public Transferable getTransferable() {
+ return this.transferable;
+ }
+
+ public void setTransferable(Transferable transferable) {
+ this.transferable = transferable;
+ }
+
+ public boolean similar(Message message) {
+ if (type != TYPE_PRIVATE && this.serverMsgId != null && message.getServerMsgId() != null) {
+ return this.serverMsgId.equals(message.getServerMsgId());
+ } else if (this.body == null || this.counterpart == null) {
+ return false;
+ } else {
+ String body, otherBody;
+ if (this.hasFileOnRemoteHost()) {
+ body = getFileParams().url.toString();
+ otherBody = message.body == null ? null : message.body.trim();
+ } else {
+ body = this.body;
+ otherBody = message.body;
+ }
+ if (message.getRemoteMsgId() != null) {
+ return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid))
+ && this.counterpart.equals(message.getCounterpart())
+ && (body.equals(otherBody)
+ ||(message.getEncryption() == Message.ENCRYPTION_PGP
+ && message.getRemoteMsgId().matches("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"))) ;
+ } else {
+ return this.remoteMsgId == null
+ && this.counterpart.equals(message.getCounterpart())
+ && body.equals(otherBody)
+ && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.MESSAGE_MERGE_WINDOW * 1000;
+ }
+ }
+ }
+
+ public Message next() {
+ synchronized (this.conversation.messages) {
+ if (this.mNextMessage == null) {
+ int index = this.conversation.messages.indexOf(this);
+ if (index < 0 || index >= this.conversation.messages.size() - 1) {
+ this.mNextMessage = null;
+ } else {
+ this.mNextMessage = this.conversation.messages.get(index + 1);
+ }
+ }
+ return this.mNextMessage;
+ }
+ }
+
+ public Message prev() {
+ synchronized (this.conversation.messages) {
+ if (this.mPreviousMessage == null) {
+ int index = this.conversation.messages.indexOf(this);
+ if (index <= 0 || index > this.conversation.messages.size()) {
+ this.mPreviousMessage = null;
+ } else {
+ this.mPreviousMessage = this.conversation.messages.get(index - 1);
+ }
+ }
+ return this.mPreviousMessage;
+ }
+ }
+
+ public boolean isLastCorrectableMessage() {
+ Message next = next();
+ while(next != null) {
+ if (next.isCorrectable()) {
+ return false;
+ }
+ next = next.next();
+ }
+ return isCorrectable();
+ }
+
+ private boolean isCorrectable() {
+ return getStatus() != STATUS_RECEIVED && !isCarbon();
+ }
+
+ public boolean mergeable(final Message message) {
+ return message != null &&
+ (message.getType() == Message.TYPE_TEXT &&
+ this.getTransferable() == null &&
+ message.getTransferable() == null &&
+ message.getEncryption() != Message.ENCRYPTION_PGP &&
+ message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED &&
+ this.getType() == message.getType() &&
+ //this.getStatus() == message.getStatus() &&
+ isStatusMergeable(this.getStatus(), message.getStatus()) &&
+ this.getEncryption() == message.getEncryption() &&
+ this.getCounterpart() != null &&
+ this.getCounterpart().equals(message.getCounterpart()) &&
+ this.edited() == message.edited() &&
+ (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) &&
+ this.getBody().length() + message.getBody().length() <= Config.MAX_DISPLAY_MESSAGE_CHARS &&
+ !GeoHelper.isGeoUri(message.getBody()) &&
+ !GeoHelper.isGeoUri(this.body) &&
+ message.treatAsDownloadable() == Decision.NEVER &&
+ this.treatAsDownloadable() == Decision.NEVER &&
+ !message.getBody().startsWith(ME_COMMAND) &&
+ !this.getBody().startsWith(ME_COMMAND) &&
+ !this.bodyIsHeart() &&
+ !message.bodyIsHeart() &&
+ this.isTrusted() == message.isTrusted()
+ );
+ }
+
+ private static boolean isStatusMergeable(int a, int b) {
+ return a == b || (
+ (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_UNSEND)
+ || (a == Message.STATUS_SEND_RECEIVED && b == Message.STATUS_SEND)
+ || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND)
+ || (a == Message.STATUS_UNSEND && b == Message.STATUS_SEND_RECEIVED)
+ || (a == Message.STATUS_SEND && b == Message.STATUS_UNSEND)
+ || (a == Message.STATUS_SEND && b == Message.STATUS_SEND_RECEIVED)
+ );
+ }
+
+ public String getMergedBody() {
+ StringBuilder body = new StringBuilder(this.body.trim());
+ Message current = this;
+ while(current.mergeable(current.next())) {
+ current = current.next();
+ if (current == null) {
+ break;
+ }
+ body.append(MERGE_SEPARATOR);
+ body.append(current.getBody().trim());
+ }
+ return body.toString();
+ }
+
+ public boolean hasMeCommand() {
+ return getMergedBody().startsWith(ME_COMMAND);
+ }
+
+ public int getMergedStatus() {
+ int status = this.status;
+ Message current = this;
+ while(current.mergeable(current.next())) {
+ current = current.next();
+ if (current == null) {
+ break;
+ }
+ status = current.status;
+ }
+ return status;
+ }
+
+ public long getMergedTimeSent() {
+ long time = this.timeSent;
+ Message current = this;
+ while(current.mergeable(current.next())) {
+ current = current.next();
+ if (current == null) {
+ break;
+ }
+ time = current.timeSent;
+ }
+ return time;
+ }
+
+ public boolean wasMergedIntoPrevious() {
+ Message prev = this.prev();
+ return prev != null && prev.mergeable(this);
+ }
+
+ public boolean trusted() {
+ Contact contact = this.getContact();
+ return (status > STATUS_RECEIVED || (contact != null && contact.trusted()));
+ }
+
+ public boolean fixCounterpart() {
+ Presences presences = conversation.getContact().getPresences();
+ if (counterpart != null && presences.has(counterpart.getResourcepart())) {
+ return true;
+ } else if (presences.size() >= 1) {
+ try {
+ counterpart = Jid.fromParts(conversation.getJid().getLocalpart(),
+ conversation.getJid().getDomainpart(),
+ presences.asStringArray()[0]);
+ return true;
+ } catch (InvalidJidException e) {
+ counterpart = null;
+ return false;
+ }
+ } else {
+ counterpart = null;
+ return false;
+ }
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public String getEditedId() {
+ return edited;
+ }
+
+ public void setOob(boolean isOob) {
+ this.oob = isOob;
+ }
+
+ public enum Decision {
+ MUST,
+ SHOULD,
+ NEVER,
+ }
+
+ private static String extractRelevantExtension(URL url) {
+ String path = url.getPath();
+ return extractRelevantExtension(path);
+ }
+
+ private static String extractRelevantExtension(String path) {
+ if (path == null || path.isEmpty()) {
+ return null;
+ }
+
+ String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase();
+ int dotPosition = filename.lastIndexOf(".");
+
+ if (dotPosition != -1) {
+ String extension = filename.substring(dotPosition + 1);
+ // we want the real file extension, not the crypto one
+ if (Transferable.VALID_CRYPTO_EXTENSIONS.contains(extension)) {
+ return extractRelevantExtension(filename.substring(0,dotPosition));
+ } else {
+ return extension;
+ }
+ }
+ return null;
+ }
+
+ public String getMimeType() {
+ if (relativeFilePath != null) {
+ int start = relativeFilePath.lastIndexOf('.') + 1;
+ if (start < relativeFilePath.length()) {
+ return MimeUtils.guessMimeTypeFromExtension(relativeFilePath.substring(start));
+ } else {
+ return null;
+ }
+ } else {
+ try {
+ return MimeUtils.guessMimeTypeFromExtension(extractRelevantExtension(new URL(body.trim())));
+ } catch (MalformedURLException e) {
+ return null;
+ }
+ }
+ }
+
+ public Decision treatAsDownloadable() {
+ if (body.trim().contains(" ")) {
+ return Decision.NEVER;
+ }
+ try {
+ URL url = new URL(body);
+ if (!url.getProtocol().equalsIgnoreCase("http") && !url.getProtocol().equalsIgnoreCase("https")) {
+ return Decision.NEVER;
+ } else if (oob) {
+ return Decision.MUST;
+ }
+ String extension = extractRelevantExtension(url);
+ if (extension == null) {
+ return Decision.NEVER;
+ }
+ String ref = url.getRef();
+ boolean encrypted = ref != null && ref.matches("([A-Fa-f0-9]{2}){48}");
+
+ if (encrypted) {
+ return Decision.MUST;
+ } else if (Transferable.VALID_IMAGE_EXTENSIONS.contains(extension)
+ || Transferable.WELL_KNOWN_EXTENSIONS.contains(extension)) {
+ return Decision.SHOULD;
+ } else {
+ return Decision.NEVER;
+ }
+
+ } catch (MalformedURLException e) {
+ return Decision.NEVER;
+ }
+ }
+
+ public boolean bodyIsHeart() {
+ return body != null && UIHelper.HEARTS.contains(body.trim());
+ }
+
+ public FileParams getFileParams() {
+ FileParams params = getLegacyFileParams();
+ if (params != null) {
+ return params;
+ }
+ params = new FileParams();
+ if (this.transferable != null) {
+ params.size = this.transferable.getFileSize();
+ }
+ if (body == null) {
+ return params;
+ }
+ String parts[] = body.split("\\|");
+ switch (parts.length) {
+ case 1:
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ try {
+ params.url = new URL(parts[0]);
+ } catch (MalformedURLException e1) {
+ params.url = null;
+ }
+ }
+ break;
+ case 2:
+ case 4:
+ try {
+ params.url = new URL(parts[0]);
+ } catch (MalformedURLException e1) {
+ params.url = null;
+ }
+ try {
+ params.size = Long.parseLong(parts[1]);
+ } catch (NumberFormatException e) {
+ params.size = 0;
+ }
+ try {
+ params.width = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+ params.width = 0;
+ }
+ try {
+ params.height = Integer.parseInt(parts[3]);
+ } catch (NumberFormatException | ArrayIndexOutOfBoundsException e) {
+ params.height = 0;
+ }
+ break;
+ case 3:
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ params.size = 0;
+ }
+ try {
+ params.width = Integer.parseInt(parts[1]);
+ } catch (NumberFormatException e) {
+ params.width = 0;
+ }
+ try {
+ params.height = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ params.height = 0;
+ }
+ break;
+ }
+ return params;
+ }
+
+ public FileParams getLegacyFileParams() {
+ FileParams params = new FileParams();
+ if (body == null) {
+ return params;
+ }
+ String parts[] = body.split(",");
+ if (parts.length == 3) {
+ try {
+ params.size = Long.parseLong(parts[0]);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ try {
+ params.width = Integer.parseInt(parts[1]);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ try {
+ params.height = Integer.parseInt(parts[2]);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ return params;
+ } else {
+ return null;
+ }
+ }
+
+ public void untie() {
+ this.mNextMessage = null;
+ this.mPreviousMessage = null;
+ }
+
+ public boolean isFileOrImage() {
+ return type == TYPE_FILE || type == TYPE_IMAGE;
+ }
+
+ public boolean hasFileOnRemoteHost() {
+ return isFileOrImage() && getFileParams().url != null;
+ }
+
+ public boolean needsUploading() {
+ return isFileOrImage() && getFileParams().url == null;
+ }
+
+ public class FileParams {
+ public URL url;
+ public long size = 0;
+ public int width = 0;
+ public int height = 0;
+ }
+
+ public void setFingerprint(String fingerprint) {
+ this.axolotlFingerprint = fingerprint;
+ }
+
+ public String getFingerprint() {
+ return axolotlFingerprint;
+ }
+
+ public boolean isTrusted() {
+ XmppAxolotlSession.Trust t = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint);
+ return t != null && t.trusted();
+ }
+
+ private int getPreviousEncryption() {
+ for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){
+ if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
+ continue;
+ }
+ return iterator.getEncryption();
+ }
+ return ENCRYPTION_NONE;
+ }
+
+ private int getNextEncryption() {
+ for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){
+ if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) {
+ continue;
+ }
+ return iterator.getEncryption();
+ }
+ return conversation.getNextEncryption();
+ }
+
+ public boolean isValidInSession() {
+ int pastEncryption = getCleanedEncryption(this.getPreviousEncryption());
+ int futureEncryption = getCleanedEncryption(this.getNextEncryption());
+
+ boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE
+ || futureEncryption == ENCRYPTION_NONE
+ || pastEncryption != futureEncryption;
+
+ return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption;
+ }
+
+ private static int getCleanedEncryption(int encryption) {
+ if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) {
+ return ENCRYPTION_PGP;
+ }
+ return encryption;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/MucOptions.java b/src/main/java/de/pixart/messenger/entities/MucOptions.java
new file mode 100644
index 000000000..da2045d26
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/MucOptions.java
@@ -0,0 +1,680 @@
+package de.pixart.messenger.entities;
+
+import android.annotation.SuppressLint;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import de.pixart.messenger.R;
+import de.pixart.messenger.xmpp.forms.Data;
+import de.pixart.messenger.xmpp.forms.Field;
+import de.pixart.messenger.xmpp.jid.InvalidJidException;
+import de.pixart.messenger.xmpp.jid.Jid;
+import de.pixart.messenger.xmpp.pep.Avatar;
+
+@SuppressLint("DefaultLocale")
+public class MucOptions {
+
+ private boolean mAutoPushConfiguration = true;
+
+ public Account getAccount() {
+ return this.conversation.getAccount();
+ }
+
+ public void setSelf(User user) {
+ this.self = user;
+ }
+
+ public void changeAffiliation(Jid jid, Affiliation affiliation) {
+ User user = findUserByRealJid(jid);
+ synchronized (users) {
+ if (user != null && user.getRole() == Role.NONE) {
+ users.remove(user);
+ if (affiliation.ranks(Affiliation.MEMBER)) {
+ user.affiliation = affiliation;
+ users.add(user);
+ }
+ }
+ }
+ }
+
+ public void flagNoAutoPushConfiguration() {
+ mAutoPushConfiguration = false;
+ }
+
+ public boolean autoPushConfiguration() {
+ return mAutoPushConfiguration;
+ }
+
+ public enum Affiliation {
+ OWNER("owner", 4, R.string.owner),
+ ADMIN("admin", 3, R.string.admin),
+ MEMBER("member", 2, R.string.member),
+ OUTCAST("outcast", 0, R.string.outcast),
+ NONE("none", 1, R.string.no_affiliation);
+
+ Affiliation(String string, int rank, int resId) {
+ this.string = string;
+ this.resId = resId;
+ this.rank = rank;
+ }
+
+ private String string;
+ private int resId;
+ private int rank;
+
+ public int getResId() {
+ return resId;
+ }
+
+ @Override
+ public String toString() {
+ return this.string;
+ }
+
+ public boolean outranks(Affiliation affiliation) {
+ return rank > affiliation.rank;
+ }
+
+ public boolean ranks(Affiliation affiliation) {
+ return rank >= affiliation.rank;
+ }
+ }
+
+ public enum Role {
+ MODERATOR("moderator", R.string.moderator,3),
+ VISITOR("visitor", R.string.visitor,1),
+ PARTICIPANT("participant", R.string.participant,2),
+ NONE("none", R.string.no_role,0);
+
+ Role(String string, int resId, int rank) {
+ this.string = string;
+ this.resId = resId;
+ this.rank = rank;
+ }
+
+ private String string;
+ private int resId;
+ private int rank;
+
+ public int getResId() {
+ return resId;
+ }
+
+ @Override
+ public String toString() {
+ return this.string;
+ }
+
+ public boolean ranks(Role role) {
+ return rank >= role.rank;
+ }
+ }
+
+ public enum Error {
+ NO_RESPONSE,
+ NONE,
+ NICK_IN_USE,
+ PASSWORD_REQUIRED,
+ BANNED,
+ MEMBERS_ONLY,
+ KICKED,
+ SHUTDOWN,
+ UNKNOWN
+ }
+
+ public static final String STATUS_CODE_ROOM_CONFIG_CHANGED = "104";
+ public static final String STATUS_CODE_SELF_PRESENCE = "110";
+ public static final String STATUS_CODE_ROOM_CREATED = "201";
+ public static final String STATUS_CODE_BANNED = "301";
+ public static final String STATUS_CODE_CHANGED_NICK = "303";
+ public static final String STATUS_CODE_KICKED = "307";
+ public static final String STATUS_CODE_AFFILIATION_CHANGE = "321";
+ public static final String STATUS_CODE_LOST_MEMBERSHIP = "322";
+ public static final String STATUS_CODE_SHUTDOWN = "332";
+
+ private interface OnEventListener {
+ void onSuccess();
+
+ void onFailure();
+ }
+
+ public interface OnRenameListener extends OnEventListener {
+
+ }
+
+ public static class User implements Comparable<User> {
+ private Role role = Role.NONE;
+ private Affiliation affiliation = Affiliation.NONE;
+ private Jid realJid;
+ private Jid fullJid;
+ private long pgpKeyId = 0;
+ private Avatar avatar;
+ private MucOptions options;
+
+ public User(MucOptions options, Jid from) {
+ this.options = options;
+ this.fullJid = from;
+ }
+
+ public String getName() {
+ return fullJid == null ? null : fullJid.getResourcepart();
+ }
+
+ public void setRealJid(Jid jid) {
+ this.realJid = jid != null ? jid.toBareJid() : null;
+ }
+
+ public Role getRole() {
+ return this.role;
+ }
+
+ public void setRole(String role) {
+ if (role == null) {
+ this.role = Role.NONE;
+ return;
+ }
+ role = role.toLowerCase();
+ switch (role) {
+ case "moderator":
+ this.role = Role.MODERATOR;
+ break;
+ case "participant":
+ this.role = Role.PARTICIPANT;
+ break;
+ case "visitor":
+ this.role = Role.VISITOR;
+ break;
+ default:
+ this.role = Role.NONE;
+ break;
+ }
+ }
+
+ public Affiliation getAffiliation() {
+ return this.affiliation;
+ }
+
+ public void setAffiliation(String affiliation) {
+ if (affiliation == null) {
+ this.affiliation = Affiliation.NONE;
+ return;
+ }
+ affiliation = affiliation.toLowerCase();
+ switch (affiliation) {
+ case "admin":
+ this.affiliation = Affiliation.ADMIN;
+ break;
+ case "owner":
+ this.affiliation = Affiliation.OWNER;
+ break;
+ case "member":
+ this.affiliation = Affiliation.MEMBER;
+ break;
+ case "outcast":
+ this.affiliation = Affiliation.OUTCAST;
+ break;
+ default:
+ this.affiliation = Affiliation.NONE;
+ }
+ }
+
+ public void setPgpKeyId(long id) {
+ this.pgpKeyId = id;
+ }
+
+ public long getPgpKeyId() {
+ return this.pgpKeyId;
+ }
+
+ public Contact getContact() {
+ if (fullJid != null) {
+ return getAccount().getRoster().getContactFromRoster(realJid);
+ } else if (realJid != null){
+ return getAccount().getRoster().getContact(realJid);
+ } else {
+ return null;
+ }
+ }
+
+ public boolean setAvatar(Avatar avatar) {
+ if (this.avatar != null && this.avatar.equals(avatar)) {
+ return false;
+ } else {
+ this.avatar = avatar;
+ return true;
+ }
+ }
+
+ public String getAvatar() {
+ return avatar == null ? null : avatar.getFilename();
+ }
+
+ public Account getAccount() {
+ return options.getAccount();
+ }
+
+ public Jid getFullJid() {
+ return fullJid;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ User user = (User) o;
+
+ if (role != user.role) return false;
+ if (affiliation != user.affiliation) return false;
+ if (realJid != null ? !realJid.equals(user.realJid) : user.realJid != null)
+ return false;
+ return fullJid != null ? fullJid.equals(user.fullJid) : user.fullJid == null;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = role != null ? role.hashCode() : 0;
+ result = 31 * result + (affiliation != null ? affiliation.hashCode() : 0);
+ result = 31 * result + (realJid != null ? realJid.hashCode() : 0);
+ result = 31 * result + (fullJid != null ? fullJid.hashCode() : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "[fulljid:"+String.valueOf(fullJid)+",realjid:"+String.valueOf(realJid)+",affiliation"+affiliation.toString()+"]";
+ }
+
+ public boolean realJidMatchesAccount() {
+ return realJid != null && realJid.equals(options.account.getJid().toBareJid());
+ }
+
+ @Override
+ public int compareTo(User another) {
+ if (another.getAffiliation().outranks(getAffiliation())) {
+ return 1;
+ } else if (getAffiliation().outranks(another.getAffiliation())) {
+ return -1;
+ } else {
+ return getComparableName().compareToIgnoreCase(another.getComparableName());
+ }
+ }
+
+
+ private String getComparableName() {
+ Contact contact = getContact();
+ if (contact != null) {
+ return contact.getDisplayName();
+ } else {
+ String name = getName();
+ return name == null ? "" : name;
+ }
+ }
+
+ public Jid getRealJid() {
+ return realJid;
+ }
+ }
+
+ private Account account;
+ private final Set<User> users = new HashSet<>();
+ private final List<String> features = new ArrayList<>();
+ private Data form = new Data();
+ private Conversation conversation;
+ private boolean isOnline = false;
+ private Error error = Error.NONE;
+ public OnRenameListener onRenameListener = null;
+ private User self;
+ private String subject = null;
+ private String password = null;
+ public boolean mNickChangingInProgress = false;
+
+ public MucOptions(Conversation conversation) {
+ this.account = conversation.getAccount();
+ this.conversation = conversation;
+ this.self = new User(this,createJoinJid(getProposedNick()));
+ }
+
+ public void updateFeatures(ArrayList<String> features) {
+ this.features.clear();
+ this.features.addAll(features);
+ }
+
+ public void updateFormData(Data form) {
+ this.form = form;
+ }
+
+ public boolean hasFeature(String feature) {
+ return this.features.contains(feature);
+ }
+
+ public boolean canInvite() {
+ Field field = this.form.getFieldByName("muc#roomconfig_allowinvites");
+ return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
+ }
+
+ public boolean canChangeSubject() {
+ Field field = this.form.getFieldByName("muc#roomconfig_changesubject");
+ return self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue()));
+ }
+
+ public boolean participating() {
+ return !online()
+ || self.getRole().ranks(Role.PARTICIPANT)
+ || hasFeature("muc_unmoderated");
+ }
+
+ public boolean membersOnly() {
+ return hasFeature("muc_membersonly");
+ }
+
+ public boolean mamSupport() {
+ // Update with "urn:xmpp:mam:1" once we support it
+ return hasFeature("urn:xmpp:mam:0");
+ }
+
+ public boolean nonanonymous() {
+ return hasFeature("muc_nonanonymous");
+ }
+
+ public boolean persistent() {
+ return hasFeature("muc_persistent");
+ }
+
+ public boolean moderated() {
+ return hasFeature("muc_moderated");
+ }
+
+ public User deleteUser(Jid jid) {
+ User user = findUserByFullJid(jid);
+ if (user != null) {
+ synchronized (users) {
+ users.remove(user);
+ if (membersOnly() &&
+ nonanonymous() &&
+ user.affiliation.ranks(Affiliation.MEMBER) &&
+ user.realJid != null) {
+ user.role = Role.NONE;
+ user.avatar = null;
+ user.fullJid = null;
+ users.add(user);
+ }
+ }
+ }
+ return user;
+ }
+
+ public void addUser(User user) {
+ User old;
+ if (user.fullJid == null && user.realJid != null) {
+ old = findUserByRealJid(user.realJid);
+ if (old != null) {
+ if (old.fullJid != null) {
+ return; //don't add. user already exists
+ } else {
+ synchronized (users) {
+ users.remove(old);
+ }
+ }
+ }
+ } else if (user.realJid != null) {
+ old = findUserByRealJid(user.realJid);
+ synchronized (users) {
+ if (old != null && old.fullJid == null) {
+ users.remove(old);
+ }
+ }
+ }
+ old = findUserByFullJid(user.getFullJid());
+ synchronized (this.users) {
+ if (old != null) {
+ users.remove(old);
+ }
+ this.users.add(user);
+ }
+ }
+
+ public User findUserByFullJid(Jid jid) {
+ if (jid == null) {
+ return null;
+ }
+ synchronized (users) {
+ for (User user : users) {
+ if (jid.equals(user.getFullJid())) {
+ return user;
+ }
+ }
+ }
+ return null;
+ }
+
+ private User findUserByRealJid(Jid jid) {
+ if (jid == null) {
+ return null;
+ }
+ synchronized (users) {
+ for (User user : users) {
+ if (jid.equals(user.realJid)) {
+ return user;
+ }
+ }
+ }
+ return null;
+ }
+
+ public boolean isContactInRoom(Contact contact) {
+ return findUserByRealJid(contact.getJid().toBareJid()) != null;
+ }
+
+ public boolean isUserInRoom(Jid jid) {
+ return findUserByFullJid(jid) != null;
+ }
+
+ public void setError(Error error) {
+ this.isOnline = isOnline && error == Error.NONE;
+ this.error = error;
+ }
+
+ public void setOnline() {
+ this.isOnline = true;
+ }
+
+ public ArrayList<User> getUsers() {
+ return getUsers(true);
+ }
+
+ public ArrayList<User> getUsers(boolean includeOffline) {
+ synchronized (users) {
+ if (includeOffline) {
+ return new ArrayList<>(users);
+ } else {
+ ArrayList<User> onlineUsers = new ArrayList<>();
+ for (User user : users) {
+ if (user.getRole().ranks(Role.PARTICIPANT)) {
+ onlineUsers.add(user);
+ }
+ }
+ return onlineUsers;
+ }
+ }
+ }
+
+ public List<User> getUsers(int max) {
+ ArrayList<User> users = getUsers();
+ return users.subList(0, Math.min(max, users.size()));
+ }
+
+ public int getUserCount() {
+ synchronized (users) {
+ return users.size();
+ }
+ }
+
+ public String getProposedNick() {
+ if (conversation.getBookmark() != null
+ && conversation.getBookmark().getNick() != null
+ && !conversation.getBookmark().getNick().trim().isEmpty()) {
+ return conversation.getBookmark().getNick().trim();
+ } else if (!conversation.getJid().isBareJid()) {
+ return conversation.getJid().getResourcepart();
+ } else {
+ return account.getUsername();
+ }
+ }
+
+ public String getActualNick() {
+ if (this.self.getName() != null) {
+ return this.self.getName();
+ } else {
+ return this.getProposedNick();
+ }
+ }
+
+ public boolean online() {
+ return this.isOnline;
+ }
+
+ public Error getError() {
+ return this.error;
+ }
+
+ public void setOnRenameListener(OnRenameListener listener) {
+ this.onRenameListener = listener;
+ }
+
+ public void setOffline() {
+ synchronized (users) {
+ this.users.clear();
+ }
+ this.error = Error.NO_RESPONSE;
+ this.isOnline = false;
+ }
+
+ public User getSelf() {
+ return self;
+ }
+
+ public void setSubject(String content) {
+ this.subject = content;
+ }
+
+ public String getSubject() {
+ return this.subject;
+ }
+
+ public String createNameFromParticipants() {
+ if (users.size() >= 1) {
+ List<String> names = new ArrayList<>();
+ for (User user : getUsers(5)) {
+ Contact contact = user.getContact();
+ if (contact != null && !contact.getDisplayName().isEmpty()) {
+ names.add(contact.getDisplayName().split("\\s+")[0]);
+ } else if (user.getName() != null){
+ names.add(user.getName());
+ }
+ }
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < names.size(); ++i) {
+ builder.append(names.get(i));
+ if (i != names.size() - 1) {
+ builder.append(", ");
+ }
+ }
+ return builder.toString();
+ } else {
+ return null;
+ }
+ }
+
+ public long[] getPgpKeyIds() {
+ List<Long> ids = new ArrayList<>();
+ for (User user : this.users) {
+ if (user.getPgpKeyId() != 0) {
+ ids.add(user.getPgpKeyId());
+ }
+ }
+ ids.add(account.getPgpId());
+ long[] primitiveLongArray = new long[ids.size()];
+ for (int i = 0; i < ids.size(); ++i) {
+ primitiveLongArray[i] = ids.get(i);
+ }
+ return primitiveLongArray;
+ }
+
+ public boolean pgpKeysInUse() {
+ synchronized (users) {
+ for (User user : users) {
+ if (user.getPgpKeyId() != 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ public boolean everybodyHasKeys() {
+ synchronized (users) {
+ for (User user : users) {
+ if (user.getPgpKeyId() == 0) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ public Jid createJoinJid(String nick) {
+ try {
+ return Jid.fromString(this.conversation.getJid().toBareJid().toString() + "/" + nick);
+ } catch (final InvalidJidException e) {
+ return null;
+ }
+ }
+
+ public Jid getTrueCounterpart(Jid jid) {
+ if (jid.equals(getSelf().getFullJid())) {
+ return account.getJid().toBareJid();
+ }
+ User user = findUserByFullJid(jid);
+ return user == null ? null : user.realJid;
+ }
+
+ public String getPassword() {
+ this.password = conversation.getAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD);
+ if (this.password == null && conversation.getBookmark() != null
+ && conversation.getBookmark().getPassword() != null) {
+ return conversation.getBookmark().getPassword();
+ } else {
+ return this.password;
+ }
+ }
+
+ public void setPassword(String password) {
+ if (conversation.getBookmark() != null) {
+ conversation.getBookmark().setPassword(password);
+ } else {
+ this.password = password;
+ }
+ conversation.setAttribute(Conversation.ATTRIBUTE_MUC_PASSWORD, password);
+ }
+
+ public Conversation getConversation() {
+ return this.conversation;
+ }
+
+ public List<Jid> getMembers() {
+ ArrayList<Jid> members = new ArrayList<>();
+ synchronized (users) {
+ for (User user : users) {
+ if (user.affiliation.ranks(Affiliation.MEMBER) && user.realJid != null) {
+ members.add(user.realJid);
+ }
+ }
+ }
+ return members;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Presence.java b/src/main/java/de/pixart/messenger/entities/Presence.java
new file mode 100644
index 000000000..fe7bac6ee
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Presence.java
@@ -0,0 +1,92 @@
+package de.pixart.messenger.entities;
+
+import java.util.Locale;
+
+import de.pixart.messenger.xml.Element;
+
+public class Presence implements Comparable {
+
+ public enum Status {
+ CHAT, ONLINE, AWAY, XA, DND, OFFLINE;
+
+ public String toShowString() {
+ switch(this) {
+ case CHAT: return "chat";
+ case AWAY: return "away";
+ case XA: return "xa";
+ case DND: return "dnd";
+ }
+ return null;
+ }
+
+ public static Status fromShowString(String show) {
+ if (show == null) {
+ return ONLINE;
+ } else {
+ switch (show.toLowerCase(Locale.US)) {
+ case "away":
+ return AWAY;
+ case "xa":
+ return XA;
+ case "dnd":
+ return DND;
+ case "chat":
+ return CHAT;
+ default:
+ return ONLINE;
+ }
+ }
+ }
+ }
+
+ private final Status status;
+ private ServiceDiscoveryResult disco;
+ private final String ver;
+ private final String hash;
+ private final String message;
+
+ private Presence(Status status, String ver, String hash, String message) {
+ this.status = status;
+ this.ver = ver;
+ this.hash = hash;
+ this.message = message;
+ }
+
+ public static Presence parse(String show, Element caps, String message) {
+ final String hash = caps == null ? null : caps.getAttribute("hash");
+ final String ver = caps == null ? null : caps.getAttribute("ver");
+ return new Presence(Status.fromShowString(show), ver, hash, message);
+ }
+
+ public int compareTo(Object other) {
+ return this.status.compareTo(((Presence)other).status);
+ }
+
+ public Status getStatus() {
+ return this.status;
+ }
+
+ public boolean hasCaps() {
+ return ver != null && hash != null;
+ }
+
+ public String getVer() {
+ return this.ver;
+ }
+
+ public String getHash() {
+ return this.hash;
+ }
+
+ public String getMessage() {
+ return this.message;
+ }
+
+ public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) {
+ this.disco = disco;
+ }
+
+ public ServiceDiscoveryResult getServiceDiscoveryResult() {
+ return disco;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/PresenceTemplate.java b/src/main/java/de/pixart/messenger/entities/PresenceTemplate.java
new file mode 100644
index 000000000..b8bd167db
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/PresenceTemplate.java
@@ -0,0 +1,76 @@
+package de.pixart.messenger.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+
+public class PresenceTemplate extends AbstractEntity {
+
+ public static final String TABELNAME = "presence_templates";
+ public static final String LAST_USED = "last_used";
+ public static final String MESSAGE = "message";
+ public static final String STATUS = "status";
+
+ private long lastUsed = 0;
+ private String statusMessage;
+ private Presence.Status status = Presence.Status.ONLINE;
+
+ public PresenceTemplate(Presence.Status status, String statusMessage) {
+ this.status = status;
+ this.statusMessage = statusMessage;
+ this.lastUsed = System.currentTimeMillis();
+ this.uuid = java.util.UUID.randomUUID().toString();
+ }
+
+ private PresenceTemplate() {
+
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ final String show = status.toShowString();
+ ContentValues values = new ContentValues();
+ values.put(LAST_USED, lastUsed);
+ values.put(MESSAGE, statusMessage);
+ values.put(STATUS, show == null ? "" : show);
+ values.put(UUID, uuid);
+ return values;
+ }
+
+ public static PresenceTemplate fromCursor(Cursor cursor) {
+ PresenceTemplate template = new PresenceTemplate();
+ template.uuid = cursor.getString(cursor.getColumnIndex(UUID));
+ template.lastUsed = cursor.getLong(cursor.getColumnIndex(LAST_USED));
+ template.statusMessage = cursor.getString(cursor.getColumnIndex(MESSAGE));
+ template.status = Presence.Status.fromShowString(cursor.getString(cursor.getColumnIndex(STATUS)));
+ return template;
+ }
+
+ public Presence.Status getStatus() {
+ return status;
+ }
+
+ public String getStatusMessage() {
+ return statusMessage;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ PresenceTemplate template = (PresenceTemplate) o;
+
+ if (statusMessage != null ? !statusMessage.equals(template.statusMessage) : template.statusMessage != null)
+ return false;
+ return status == template.status;
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = statusMessage != null ? statusMessage.hashCode() : 0;
+ result = 31 * result + status.hashCode();
+ return result;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Presences.java b/src/main/java/de/pixart/messenger/entities/Presences.java
new file mode 100644
index 000000000..ecd826d41
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Presences.java
@@ -0,0 +1,102 @@
+package de.pixart.messenger.entities;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+public class Presences {
+ private final Hashtable<String, Presence> presences = new Hashtable<>();
+
+ public Hashtable<String, Presence> getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(String resource, Presence presence) {
+ synchronized (this.presences) {
+ this.presences.put(resource, presence);
+ }
+ }
+
+ public void removePresence(String resource) {
+ synchronized (this.presences) {
+ this.presences.remove(resource);
+ }
+ }
+
+ public void clearPresences() {
+ synchronized (this.presences) {
+ this.presences.clear();
+ }
+ }
+
+ public Presence.Status getShownStatus() {
+ Presence.Status status = Presence.Status.OFFLINE;
+ synchronized (this.presences) {
+ for(Presence p : presences.values()) {
+ if (p.getStatus() == Presence.Status.DND) {
+ return p.getStatus();
+ } else if (p.getStatus().compareTo(status) < 0){
+ status = p.getStatus();
+ }
+ }
+ }
+ return status;
+ }
+
+ public int size() {
+ synchronized (this.presences) {
+ return presences.size();
+ }
+ }
+
+ public String[] asStringArray() {
+ synchronized (this.presences) {
+ final String[] presencesArray = new String[presences.size()];
+ presences.keySet().toArray(presencesArray);
+ return presencesArray;
+ }
+ }
+
+ public List<PresenceTemplate> asTemplates() {
+ synchronized (this.presences) {
+ ArrayList<PresenceTemplate> templates = new ArrayList<>(presences.size());
+ for(Presence p : presences.values()) {
+ if (p.getMessage() != null && !p.getMessage().trim().isEmpty()) {
+ templates.add(new PresenceTemplate(p.getStatus(), p.getMessage()));
+ }
+ }
+ return templates;
+ }
+ }
+
+ public boolean has(String presence) {
+ synchronized (this.presences) {
+ return presences.containsKey(presence);
+ }
+ }
+
+ public List<String> getStatusMessages() {
+ ArrayList<String> messages = new ArrayList<>();
+ synchronized (this.presences) {
+ for(Presence presence : this.presences.values()) {
+ String message = presence.getMessage() == null ? null : presence.getMessage().trim();
+ if (message != null && !message.isEmpty() && !messages.contains(message)) {
+ messages.add(message);
+ }
+ }
+ }
+ return messages;
+ }
+
+ public boolean allOrNonSupport(String namespace) {
+ synchronized (this.presences) {
+ for(Presence presence : this.presences.values()) {
+ ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
+ if (disco == null || !disco.getFeatures().contains(namespace)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Roster.java b/src/main/java/de/pixart/messenger/entities/Roster.java
new file mode 100644
index 000000000..e267142bc
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Roster.java
@@ -0,0 +1,96 @@
+package de.pixart.messenger.entities;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+import de.pixart.messenger.xmpp.jid.Jid;
+
+public class Roster {
+ final Account account;
+ final HashMap<Jid, Contact> contacts = new HashMap<>();
+ private String version = null;
+
+ public Roster(Account account) {
+ this.account = account;
+ }
+
+ public Contact getContactFromRoster(Jid jid) {
+ if (jid == null) {
+ return null;
+ }
+ synchronized (this.contacts) {
+ Contact contact = contacts.get(jid.toBareJid());
+ if (contact != null && contact.showInRoster()) {
+ return contact;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ public Contact getContact(final Jid jid) {
+ synchronized (this.contacts) {
+ if (!contacts.containsKey(jid.toBareJid())) {
+ Contact contact = new Contact(jid.toBareJid());
+ contact.setAccount(account);
+ contacts.put(contact.getJid().toBareJid(), contact);
+ return contact;
+ }
+ return contacts.get(jid.toBareJid());
+ }
+ }
+
+ public void clearPresences() {
+ for (Contact contact : getContacts()) {
+ contact.clearPresences();
+ }
+ }
+
+ public void markAllAsNotInRoster() {
+ for (Contact contact : getContacts()) {
+ contact.resetOption(Contact.Options.IN_ROSTER);
+ }
+ }
+
+ public List<Contact> getWithSystemAccounts() {
+ List<Contact> with = getContacts();
+ for(Iterator<Contact> iterator = with.iterator(); iterator.hasNext();) {
+ Contact contact = iterator.next();
+ if (contact.getSystemAccount() == null) {
+ iterator.remove();
+ }
+ }
+ return with;
+ }
+
+ public List<Contact> getContacts() {
+ synchronized (this.contacts) {
+ return new ArrayList<>(this.contacts.values());
+ }
+ }
+
+ public void initContact(final Contact contact) {
+ if (contact == null) {
+ return;
+ }
+ contact.setAccount(account);
+ contact.setOption(Contact.Options.IN_ROSTER);
+ synchronized (this.contacts) {
+ contacts.put(contact.getJid().toBareJid(), contact);
+ }
+ }
+
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ public String getVersion() {
+ return this.version;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/ServiceDiscoveryResult.java b/src/main/java/de/pixart/messenger/entities/ServiceDiscoveryResult.java
new file mode 100644
index 000000000..cac20ba90
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/ServiceDiscoveryResult.java
@@ -0,0 +1,349 @@
+package de.pixart.messenger.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.util.Base64;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import de.pixart.messenger.xml.Element;
+import de.pixart.messenger.xmpp.forms.Data;
+import de.pixart.messenger.xmpp.forms.Field;
+import de.pixart.messenger.xmpp.stanzas.IqPacket;
+
+public class ServiceDiscoveryResult {
+ public static final String TABLENAME = "discovery_results";
+ public static final String HASH = "hash";
+ public static final String VER = "ver";
+ public static final String RESULT = "result";
+
+ protected static String blankNull(String s) {
+ return s == null ? "" : s;
+ }
+
+ public static class Identity implements Comparable {
+ protected final String category;
+ protected final String type;
+ protected final String lang;
+ protected final String name;
+
+ public Identity(final String category, final String type, final String lang, final String name) {
+ this.category = category;
+ this.type = type;
+ this.lang = lang;
+ this.name = name;
+ }
+
+ public Identity(final Element el) {
+ this(
+ el.getAttribute("category"),
+ el.getAttribute("type"),
+ el.getAttribute("xml:lang"),
+ el.getAttribute("name")
+ );
+ }
+
+ public Identity(final JSONObject o) {
+
+ this(
+ o.optString("category", null),
+ o.optString("type", null),
+ o.optString("lang", null),
+ o.optString("name", null)
+ );
+ }
+
+ public String getCategory() {
+ return this.category;
+ }
+
+ public String getType() {
+ return this.type;
+ }
+
+ public String getLang() {
+ return this.lang;
+ }
+
+ public String getName() {
+ return this.name;
+ }
+
+ public int compareTo(Object other) {
+ Identity o = (Identity)other;
+ int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory()));
+ if(r == 0) {
+ r = blankNull(this.getType()).compareTo(blankNull(o.getType()));
+ }
+ if(r == 0) {
+ r = blankNull(this.getLang()).compareTo(blankNull(o.getLang()));
+ }
+ if(r == 0) {
+ r = blankNull(this.getName()).compareTo(blankNull(o.getName()));
+ }
+
+ return r;
+ }
+
+ public JSONObject toJSON() {
+ try {
+ JSONObject o = new JSONObject();
+ o.put("category", this.getCategory());
+ o.put("type", this.getType());
+ o.put("lang", this.getLang());
+ o.put("name", this.getName());
+ return o;
+ } catch(JSONException e) {
+ return null;
+ }
+ }
+ }
+
+ protected final String hash;
+ protected final byte[] ver;
+ protected final List<Identity> identities;
+ protected final List<String> features;
+ protected final List<Data> forms;
+
+ public ServiceDiscoveryResult(final IqPacket packet) {
+ this.identities = new ArrayList<>();
+ this.features = new ArrayList<>();
+ this.forms = new ArrayList<>();
+ this.hash = "sha-1"; // We only support sha-1 for now
+
+ final List<Element> elements = packet.query().getChildren();
+
+ for (final Element element : elements) {
+ if (element.getName().equals("identity")) {
+ Identity id = new Identity(element);
+ if (id.getType() != null && id.getCategory() != null) {
+ identities.add(id);
+ }
+ } else if (element.getName().equals("feature")) {
+ if (element.getAttribute("var") != null) {
+ features.add(element.getAttribute("var"));
+ }
+ } else if (element.getName().equals("x") && "jabber:x:data".equals(element.getAttribute("xmlns"))) {
+ forms.add(Data.parse(element));
+ }
+ }
+ this.ver = this.mkCapHash();
+ }
+
+ public ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException {
+ this.identities = new ArrayList<>();
+ this.features = new ArrayList<>();
+ this.forms = new ArrayList<>();
+ this.hash = hash;
+ this.ver = ver;
+
+ JSONArray identities = o.optJSONArray("identities");
+ if (identities != null) {
+ for (int i = 0; i < identities.length(); i++) {
+ this.identities.add(new Identity(identities.getJSONObject(i)));
+ }
+ }
+ JSONArray features = o.optJSONArray("features");
+ if (features != null) {
+ for (int i = 0; i < features.length(); i++) {
+ this.features.add(features.getString(i));
+ }
+ }
+ JSONArray forms = o.optJSONArray("forms");
+ if (forms != null) {
+ for(int i = 0; i < forms.length(); i++) {
+ this.forms.add(createFormFromJSONObject(forms.getJSONObject(i)));
+ }
+ }
+ }
+
+ private static Data createFormFromJSONObject(JSONObject o) {
+ Data data = new Data();
+ JSONArray names = o.names();
+ for(int i = 0; i < names.length(); ++i) {
+ try {
+ String name = names.getString(i);
+ JSONArray jsonValues = o.getJSONArray(name);
+ ArrayList<String> values = new ArrayList<>(jsonValues.length());
+ for(int j = 0; j < jsonValues.length(); ++j) {
+ values.add(jsonValues.getString(j));
+ }
+ data.put(name, values);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return data;
+ }
+
+ private static JSONObject createJSONFromForm(Data data) {
+ JSONObject object = new JSONObject();
+ for(Field field : data.getFields()) {
+ try {
+ JSONArray jsonValues = new JSONArray();
+ for(String value : field.getValues()) {
+ jsonValues.put(value);
+ }
+ object.put(field.getFieldName(), jsonValues);
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+ try {
+ JSONArray jsonValues = new JSONArray();
+ jsonValues.put(data.getFormType());
+ object.put(Data.FORM_TYPE, jsonValues);
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ return object;
+ }
+
+ public String getVer() {
+ return new String(Base64.encode(this.ver, Base64.DEFAULT)).trim();
+ }
+
+ public ServiceDiscoveryResult(Cursor cursor) throws JSONException {
+ this(
+ cursor.getString(cursor.getColumnIndex(HASH)),
+ Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT),
+ new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT)))
+ );
+ }
+
+ public List<Identity> getIdentities() {
+ return this.identities;
+ }
+
+ public List<String> getFeatures() {
+ return this.features;
+ }
+
+ public boolean hasIdentity(String category, String type) {
+ for(Identity id : this.getIdentities()) {
+ if((category == null || id.getCategory().equals(category)) &&
+ (type == null || id.getType().equals(type))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public String getExtendedDiscoInformation(String formType, String name) {
+ for(Data form : this.forms) {
+ if (formType.equals(form.getFormType())) {
+ for(Field field: form.getFields()) {
+ if (name.equals(field.getFieldName())) {
+ return field.getValue();
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ protected byte[] mkCapHash() {
+ StringBuilder s = new StringBuilder();
+
+ List<Identity> identities = this.getIdentities();
+ Collections.sort(identities);
+
+ for(Identity id : identities) {
+ s.append(
+ blankNull(id.getCategory()) + "/" +
+ blankNull(id.getType()) + "/" +
+ blankNull(id.getLang()) + "/" +
+ blankNull(id.getName()) + "<"
+ );
+ }
+
+ List<String> features = this.getFeatures();
+ Collections.sort(features);
+
+ for (String feature : features) {
+ s.append(feature + "<");
+ }
+
+ Collections.sort(forms, new Comparator<Data>() {
+ @Override
+ public int compare(Data lhs, Data rhs) {
+ return lhs.getFormType().compareTo(rhs.getFormType());
+ }
+ });
+
+ for(Data form : forms) {
+ s.append(form.getFormType() + "<");
+ List<Field> fields = form.getFields();
+ Collections.sort(fields, new Comparator<Field>() {
+ @Override
+ public int compare(Field lhs, Field rhs) {
+ return lhs.getFieldName().compareTo(rhs.getFieldName());
+ }
+ });
+ for(Field field : fields) {
+ s.append(field.getFieldName()+"<");
+ List<String> values = field.getValues();
+ Collections.sort(values);
+ for(String value : values) {
+ s.append(value+"<");
+ }
+ }
+ }
+
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ return null;
+ }
+
+ try {
+ return md.digest(s.toString().getBytes("UTF-8"));
+ } catch(UnsupportedEncodingException e) {
+ return null;
+ }
+ }
+
+ public JSONObject toJSON() {
+ try {
+ JSONObject o = new JSONObject();
+
+ JSONArray ids = new JSONArray();
+ for(Identity id : this.getIdentities()) {
+ ids.put(id.toJSON());
+ }
+ o.put("identities", ids);
+
+ o.put("features", new JSONArray(this.getFeatures()));
+
+ JSONArray forms = new JSONArray();
+ for(Data data : this.forms) {
+ forms.put(createJSONFromForm(data));
+ }
+ o.put("forms", forms);
+
+ return o;
+ } catch(JSONException e) {
+ return null;
+ }
+ }
+
+ public ContentValues getContentValues() {
+ final ContentValues values = new ContentValues();
+ values.put(HASH, this.hash);
+ values.put(VER, getVer());
+ values.put(RESULT, this.toJSON().toString());
+ return values;
+ }
+}
diff --git a/src/main/java/de/pixart/messenger/entities/Transferable.java b/src/main/java/de/pixart/messenger/entities/Transferable.java
new file mode 100644
index 000000000..14132ac38
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/Transferable.java
@@ -0,0 +1,78 @@
+package de.pixart.messenger.entities;
+
+import java.util.Arrays;
+import java.util.List;
+
+public interface Transferable {
+ List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList(
+ "webp",
+ "jpeg",
+ "jpg",
+ "png",
+ "jpe",
+ "gif",
+ "tif"
+ );
+ List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList(
+ "pgp",
+ "gpg",
+ "otr"
+ );
+ List<String> WELL_KNOWN_EXTENSIONS = Arrays.asList(
+ //documents
+ "pdf",
+ "doc",
+ "docx",
+ "txt",
+ //audio
+ "m4a",
+ "m4b",
+ "mp3",
+ "mp2",
+ "wav",
+ "aac",
+ "aif",
+ "aiff",
+ "aifc",
+ "mid",
+ "midi",
+ "3gpp",
+ //video
+ "avi",
+ "mp4",
+ "mpeg",
+ "mpg",
+ "mpe",
+ "mov",
+ "3gp",
+ //applications
+ "apk",
+ //contact
+ "vcf",
+ //calendar
+ "ics",
+ //compressed
+ "zip",
+ "rar"
+ );
+
+ int STATUS_UNKNOWN = 0x200;
+ int STATUS_CHECKING = 0x201;
+ int STATUS_FAILED = 0x202;
+ int STATUS_OFFER = 0x203;
+ int STATUS_DOWNLOADING = 0x204;
+ int STATUS_DELETED = 0x205;
+ int STATUS_OFFER_CHECK_FILESIZE = 0x206;
+ int STATUS_UPLOADING = 0x207;
+
+
+ boolean start();
+
+ int getStatus();
+
+ long getFileSize();
+
+ int getProgress();
+
+ void cancel();
+}
diff --git a/src/main/java/de/pixart/messenger/entities/TransferablePlaceholder.java b/src/main/java/de/pixart/messenger/entities/TransferablePlaceholder.java
new file mode 100644
index 000000000..59f1dc846
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/entities/TransferablePlaceholder.java
@@ -0,0 +1,34 @@
+package de.pixart.messenger.entities;
+
+public class TransferablePlaceholder implements Transferable {
+
+ private int status;
+
+ public TransferablePlaceholder(int status) {
+ this.status = status;
+ }
+ @Override
+ public boolean start() {
+ return false;
+ }
+
+ @Override
+ public int getStatus() {
+ return status;
+ }
+
+ @Override
+ public long getFileSize() {
+ return 0;
+ }
+
+ @Override
+ public int getProgress() {
+ return 0;
+ }
+
+ @Override
+ public void cancel() {
+
+ }
+}