diff options
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus/entities')
16 files changed, 4299 insertions, 0 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/AbstractEntity.java b/src/main/java/de/thedevstack/conversationsplus/entities/AbstractEntity.java new file mode 100644 index 00000000..ebc0a8d5 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/AbstractEntity.java @@ -0,0 +1,20 @@ +package de.thedevstack.conversationsplus.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/thedevstack/conversationsplus/entities/Account.java b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java new file mode 100644 index 00000000..f7dee013 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java @@ -0,0 +1,550 @@ +package de.thedevstack.conversationsplus.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.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.OtrService; +import de.thedevstack.conversationsplus.crypto.PgpDecryptionService; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceImpl; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceStub; +import de.thedevstack.conversationsplus.services.XmppConnectionService; +import de.thedevstack.conversationsplus.xmpp.XmppConnection; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.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 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 final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>(); + + public boolean httpUploadAvailable() { + return xmppConnection != null && xmppConnection.getFeatures().httpUpload(); + } + + 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 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); + + private final boolean isError; + + public boolean isError() { + return this.isError; + } + + private State(final boolean isError) { + this.isError = isError; + } + + private 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; + 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<>(); + + public Account() { + this.uuid = "0"; + } + + public Account(final Jid jid, final String password) { + this(java.util.UUID.randomUUID().toString(), jid, + password, 0, null, "", null, null, null, 5222); + } + + 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) { + 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; + } + + 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))); + } + + 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 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() && getXmppConnection().getAttempt() >= 3; + } + + 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); + return values; + } + + public AxolotlService getAxolotlService() { + return axolotlService; + } + + public void initAccountServices(final XmppConnectionService context) { + this.mOtrService = new OtrService(context, this); + if (ConversationsPlusPreferences.omemoEnabled()) { + this.axolotlService = new AxolotlServiceImpl(this, context); + if (xmppConnection != null) { + xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); + } + } else { + this.axolotlService = new AxolotlServiceStub(); + } + + this.pgpDecryptionService = new PgpDecryptionService(context); + } + + public OtrService getOtrService() { + return this.mOtrService; + } + + public PgpDecryptionService getPgpDecryptionService() { + return 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.getRoster().getContact(this.getJid().toBareJid()).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 -1; + } + } else { + return -1; + } + } + + 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() { + this.mEndGracePeriod = SystemClock.elapsedRealtime() + + (Config.CARBON_GRACE_PERIOD * 1000); + } + + 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/thedevstack/conversationsplus/entities/Blockable.java b/src/main/java/de/thedevstack/conversationsplus/entities/Blockable.java new file mode 100644 index 00000000..beff901d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Blockable.java @@ -0,0 +1,11 @@ +package de.thedevstack.conversationsplus.entities; + +import de.thedevstack.conversationsplus.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/thedevstack/conversationsplus/entities/Bookmark.java b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java new file mode 100644 index 00000000..07a77eae --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java @@ -0,0 +1,176 @@ +package de.thedevstack.conversationsplus.entities; + +import android.graphics.Color; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.utils.UIHelper; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.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 + && (this.mJoinedConversation.getMucOptions().getSubject() != null)) { + return this.mJoinedConversation.getMucOptions().getSubject(); + } else if (getBookmarkName() != null) { + return getBookmarkName(); + } else { + return this.getJid().getLocalpart(); + } + } + + @Override + public String getDisplayJid() { + Jid jid = getJid(); + if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.CONFERENCE_DOMAIN_LOCK)) { + return jid.getLocalpart(); + } else if (jid != null) { + return jid.toString(); + } else { + return null; + } + } + + @Override + public Jid getJid() { + return this.getAttributeAsJid("jid"); + } + + @Override + public List<Tag> getTags() { + ArrayList<Tag> tags = new ArrayList<Tag>(); + 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; + } + + @Override + public int getStatusColor() { + return Color.parseColor("#259B23"); + } + + 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); + } + } + + public boolean match(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(needle); + } + + private boolean matchInTag(String needle) { + needle = needle.toLowerCase(Locale.US); + for (Tag tag : getTags()) { + 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/thedevstack/conversationsplus/entities/Contact.java b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java new file mode 100644 index 00000000..74d76f13 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java @@ -0,0 +1,562 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; +import android.graphics.Color; + +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.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.utils.UIHelper; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.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"; + public Lastseen lastseen = new Lastseen(); + 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; + + 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 Lastseen lastseen, 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.lastseen = lastseen; + } + + public Contact(final Jid jid) { + this.jid = jid; + } + + public static Contact fromCursor(final Cursor cursor) { + final Lastseen lastseen = new Lastseen( + cursor.getString(cursor.getColumnIndex(LAST_PRESENCE)), + cursor.getLong(cursor.getColumnIndex(LAST_TIME))); + 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)), + lastseen, + 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 (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.DOMAIN_LOCK)) { + return jid.getLocalpart(); + } else 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() { + final ArrayList<Tag> tags = new ArrayList<>(); + for (final String group : getGroups()) { + tags.add(new Tag(group, UIHelper.getColorForName(group))); + } + switch (getMostAvailableStatus()) { + case CHAT: + case ONLINE: + tags.add(new Tag("online", 0xff259b24)); + break; + case AWAY: + tags.add(new Tag("away", 0xffff9800)); + break; + case XA: + tags.add(new Tag("not available", 0xfff44336)); + break; + case DND: + tags.add(new Tag("dnd", 0xfff44336)); + break; + } + if (isBlocked()) { + tags.add(new Tag("blocked", 0xff2e2f3b)); + } + return tags; + } + + @Override + public int getStatusColor() { + return Color.parseColor(UIHelper.getStatusColor(getMostAvailableStatus())); + } + + public boolean match(String needle) { + if (needle == null || needle.isEmpty()) { + return true; + } + needle = needle.toLowerCase(Locale.US); + String[] parts = needle.split("\\s+"); + if (parts.length > 1) { + for(int i = 0; i < parts.length; ++i) { + if (!match(parts[i])) { + return false; + } + } + return true; + } else { + return jid.toString().contains(needle) || + getDisplayName().toLowerCase(Locale.US).contains(needle) || + matchInTag(needle); + } + } + + private boolean matchInTag(String needle) { + needle = needle.toLowerCase(Locale.US); + for (Tag tag : getTags()) { + 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, lastseen.presence); + values.put(LAST_TIME, lastseen.time); + 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 getMostAvailableStatus() { + Presence p = this.presences.getMostAvailablePresence(); + if (p == null) { + return Presence.Status.OFFLINE; + } + + return p.getStatus(); + } + + 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); + } + + public String getShareableUri() { + if (getOtrFingerprints().size() >= 1) { + String otr = getOtrFingerprints().get(0); + return "xmpp:" + getJid().toBareJid().toString() + "?otr-fingerprint=" + otr; + } else { + return "xmpp:" + getJid().toBareJid().toString(); + } + } + + @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 static class Lastseen { + public long time; + public String presence; + + public Lastseen() { + this(null, 0); + } + + public Lastseen(final String presence, final long time) { + this.presence = presence; + this.time = time; + } + } + + 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/thedevstack/conversationsplus/entities/Conversation.java b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java new file mode 100644 index 00000000..b822ef0e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java @@ -0,0 +1,962 @@ +package de.thedevstack.conversationsplus.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.thedevstack.conversationsplus.utils.MessageUtil; +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; +import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class Conversation extends AbstractEntity implements Blockable { + 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 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.getType() == Message.TYPE_IMAGE || message.getType() == Message.TYPE_FILE) + && message.getEncryption() != Message.ENCRYPTION_PGP + && message.getUuid().equals(uuid)) { + 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) { + this.messages.subList(0, size - maxsize).clear(); + } + } + } + + 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 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 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) { + return bookmark.getBookmarkName(); + } else { + String generatedName = getMucOptions().createNameFromParticipants(); + if (generatedName != null) { + return generatedName; + } else { + return getJid().getLocalpart(); + } + } + } else { + return this.getContact().getDisplayName(); + } + } + + 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 getMostRecentlyUsedOutgoingEncryption() { + synchronized (this.messages) { + for(int i = this.messages.size() -1; i >= 0; --i) { + final Message m = this.messages.get(i); + if (!m.isCarbon() && 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; + } + + 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.X509_VERIFICATION) { + if (axolotlService != null && axolotlService.isConversationAxolotlCapable(this)) { + return Message.ENCRYPTION_AXOLOTL; + } else { + return Message.ENCRYPTION_NONE; + } + } + int outgoing = this.getMostRecentlyUsedOutgoingEncryption(); + if (outgoing == Message.ENCRYPTION_NONE) { + next = this.getMostRecentlyUsedIncomingEncryption(); + } else { + next = outgoing; + } + } + + 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).equals(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() { + 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().addAll(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; + } + } + }); + for(Message message : this.messages) { + message.untie(); + } + } + } + + public int unreadCount() { + if (getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0) == Long.MAX_VALUE) { + return 0; + } + synchronized (this.messages) { + int count = 0; + for(int i = this.messages.size() - 1; i >= 0; --i) { + Message message = this.messages.get(i); + if (message.isRead()) { + return count; + } + if (alwaysNotify() || MessageUtil.wasHighlightedOrPrivate(message)) { + ++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/thedevstack/conversationsplus/entities/DownloadableFile.java b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java new file mode 100644 index 00000000..424d0301 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java @@ -0,0 +1,87 @@ +package de.thedevstack.conversationsplus.entities; + +import java.io.File; + +import de.thedevstack.conversationsplus.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) { + byte[] secretKey = new byte[32]; + byte[] iv = new byte[16]; + System.arraycopy(keyIvCombo, 0, iv, 0, 16); + System.arraycopy(keyIvCombo, 16, secretKey, 0, 32); + this.aeskey = secretKey; + this.iv = iv; + } else if (keyIvCombo.length >= 32) { + byte[] secretKey = new byte[32]; + System.arraycopy(keyIvCombo, 0, secretKey, 0, 32); + this.aeskey = secretKey; + } else if (keyIvCombo.length >= 16) { + byte[] secretKey = new byte[16]; + System.arraycopy(keyIvCombo, 0, secretKey, 0, 16); + this.aeskey = secretKey; + } + } + + 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/thedevstack/conversationsplus/entities/ListItem.java b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java new file mode 100644 index 00000000..24dc7e94 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java @@ -0,0 +1,37 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.List; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public interface ListItem extends Comparable<ListItem> { + String getDisplayName(); + + String getDisplayJid(); + + Jid getJid(); + + public int getStatusColor(); + + List<Tag> getTags(); + + 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(final String needle); +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java new file mode 100644 index 00000000..f1da885e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java @@ -0,0 +1,816 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; + +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Arrays; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlSession; +import de.thedevstack.conversationsplus.utils.GeoHelper; +import de.thedevstack.conversationsplus.utils.MimeUtils; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class Message extends AbstractEntity { + + public static final String TABLENAME = "messages"; + + public static final String MERGE_SEPARATOR = " \u200B\n\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; + } + + @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 equals(Message message) { + if (this.serverMsgId != null && message.getServerMsgId() != null) { + return this.serverMsgId.equals(message.getServerMsgId()); + } else if (this.getBody() == null || this.counterpart == null) { + return false; + } else { + String body, otherBody; + if (this.hasFileOnRemoteHost()) { + body = getFileParams().url.toString(); + otherBody = message.getBody() == null ? null : message.getBody(); + } else { + body = this.getBody(); + otherBody = message.getBody(); + } + 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) && + !GeoHelper.isGeoUri(message.getBody()) && + !GeoHelper.isGeoUri(this.getBody()) && + message.treatAsDownloadable() == Decision.NEVER && + this.treatAsDownloadable() == Decision.NEVER && + !message.getBody().startsWith(ME_COMMAND) && + !this.getBody().startsWith(ME_COMMAND) && + 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); + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + body.append(MERGE_SEPARATOR); + body.append(current.getBody()); + } + 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(); + status = current.status; + } + return status; + } + + public long getMergedTimeSent() { + long time = this.timeSent; + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + 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(this.getBody()))); + } catch (MalformedURLException e) { + return null; + } + } + } + + public Decision treatAsDownloadable() { + /** + * there are a few cases where spaces result in an unwanted behavior, e.g. + * "http://example.com/image.jpg" text that will not be shown /abc.png" + * or more than one image link in one message. + */ + if (getBody().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) { + if (MimeUtils.guessMimeTypeFromExtension(extension) != null) { + return Decision.MUST; + } else { + return Decision.NEVER; + } + } 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 FileParams getFileParams() { + FileParams params = getLegacyFileParams(); + if (params != null) { + return params; + } + params = new FileParams(); + if (this.transferable != null) { + params.size = this.transferable.getFileSize(); + } + if (this.getBody() == null) { + return params; + } + String parts[] = this.getBody().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 (this.getBody() == null) { + return params; + } + String parts[] = this.getBody().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 setAxolotlFingerprint(String fingerprint) { + this.axolotlFingerprint = fingerprint; + } + + public String getAxolotlFingerprint() { + 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/thedevstack/conversationsplus/entities/MucOptions.java b/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java new file mode 100644 index 00000000..128e5423 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java @@ -0,0 +1,515 @@ +package de.thedevstack.conversationsplus.entities; + +import android.annotation.SuppressLint; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.xmpp.forms.Data; +import de.thedevstack.conversationsplus.xmpp.forms.Field; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; +import de.thedevstack.conversationsplus.xmpp.pep.Avatar; + +@SuppressLint("DefaultLocale") +public class MucOptions { + + public Account getAccount() { + return this.conversation.getAccount(); + } + + public void setSelf(User user) { + this.self = user; + } + + 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_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 { + private Role role = Role.NONE; + private Affiliation affiliation = Affiliation.NONE; + private Jid jid; + 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 this.fullJid.getResourcepart(); + } + + public void setJid(Jid jid) { + this.jid = jid; + } + + public Jid getJid() { + return this.jid; + } + + public Role getRole() { + return this.role; + } + + public void setRole(String role) { + 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; + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } else if (!(other instanceof User)) { + return false; + } else { + User o = (User) other; + return getName() != null && getName().equals(o.getName()) + && jid != null && jid.equals(o.jid) + && affiliation == o.affiliation + && role == o.role; + } + } + + public Affiliation getAffiliation() { + return this.affiliation; + } + + public void setAffiliation(String affiliation) { + 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() { + return getAccount().getRoster().getContactFromRoster(getJid()); + } + + 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; + } + } + + private Account account; + private final Map<String, User> users = Collections.synchronizedMap(new LinkedHashMap<String, User>()); + private final Set<Jid> members = Collections.synchronizedSet(new HashSet<Jid>()); + 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(String name) { + return this.users.remove(name); + } + + public void addUser(User user) { + this.users.put(user.getName(), user); + } + + public User findUser(String name) { + return this.users.get(name); + } + + public boolean isUserInRoom(String name) { + return findUser(name) != 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 new ArrayList<>(users.values()); + } + + public List<User> getUsers(int max) { + ArrayList<User> users = new ArrayList<>(); + int i = 1; + for(User user : this.users.values()) { + users.add(user); + if (i >= max) { + break; + } else { + ++i; + } + } + return users; + } + + public int getUserCount() { + return this.users.size(); + } + + public String getProposedNick() { + if (conversation.getBookmark() != null + && conversation.getBookmark().getNick() != null + && !conversation.getBookmark().getNick().isEmpty()) { + return conversation.getBookmark().getNick(); + } 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() { + 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() >= 2) { + 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 { + 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.values()) { + 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() { + for (User user : this.users.values()) { + if (user.getPgpKeyId() != 0) { + return true; + } + } + return false; + } + + public boolean everybodyHasKeys() { + for (User user : this.users.values()) { + 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(String name) { + User user = findUser(name); + return user == null ? null : user.getJid(); + } + + 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 void putMember(Jid jid) { + members.add(jid); + } + + public List<Jid> getMembers() { + return new ArrayList<>(members); + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Presence.java b/src/main/java/de/thedevstack/conversationsplus/entities/Presence.java new file mode 100644 index 00000000..d4f2871d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Presence.java @@ -0,0 +1,80 @@ +package de.thedevstack.conversationsplus.entities; + +import java.lang.Comparable; +import java.util.Locale; + +import de.thedevstack.conversationsplus.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; + } + } + + protected final Status status; + protected ServiceDiscoveryResult disco; + protected final String ver; + protected final String hash; + + private Presence(Status status, String ver, String hash) { + this.status = status; + this.ver = ver; + this.hash = hash; + } + + public static Presence parse(String show, Element caps) { + final String hash = caps == null ? null : caps.getAttribute("hash"); + final String ver = caps == null ? null : caps.getAttribute("ver"); + if (show == null) { + return new Presence(Status.ONLINE, ver, hash); + } else { + switch (show.toLowerCase(Locale.US)) { + case "away": + return new Presence(Status.AWAY, ver, hash); + case "xa": + return new Presence(Status.XA, ver, hash); + case "dnd": + return new Presence(Status.DND, ver, hash); + case "chat": + return new Presence(Status.CHAT, ver, hash); + default: + return new Presence(Status.ONLINE, ver, hash); + } + } + } + + 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 void setServiceDiscoveryResult(ServiceDiscoveryResult disco) { + this.disco = disco; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java b/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java new file mode 100644 index 00000000..d32e931c --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java @@ -0,0 +1,57 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.Collections; +import java.util.Hashtable; + +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 getMostAvailablePresence() { + synchronized (this.presences) { + if (presences.size() < 1) { return null; } + return Collections.min(presences.values()); + } + } + + 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 boolean has(String presence) { + synchronized (this.presences) { + return presences.containsKey(presence); + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Roster.java b/src/main/java/de/thedevstack/conversationsplus/entities/Roster.java new file mode 100644 index 00000000..ec1a9426 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Roster.java @@ -0,0 +1,96 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; + +import de.thedevstack.conversationsplus.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/thedevstack/conversationsplus/entities/ServiceDiscoveryResult.java b/src/main/java/de/thedevstack/conversationsplus/entities/ServiceDiscoveryResult.java new file mode 100644 index 00000000..cfba7c4f --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/ServiceDiscoveryResult.java @@ -0,0 +1,265 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; +import android.util.Base64; +import java.io.UnsupportedEncodingException; +import java.lang.Comparable; +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 org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.forms.Data; +import de.thedevstack.conversationsplus.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)); + } + } + } + + public String getVer() { + return new String(Base64.encode(this.ver, Base64.DEFAULT)); + } + + 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; + } + + 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()+"<"); + //TODO append fields and values + } + + 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("identites", ids); + + o.put("features", new JSONArray(this.getFeatures())); + + 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/thedevstack/conversationsplus/entities/Transferable.java b/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java new file mode 100644 index 00000000..8e2ca20e --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java @@ -0,0 +1,31 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.Arrays; +import java.util.List; + +public interface Transferable { + + List<String> VALID_IMAGE_EXTENSIONS = Arrays.asList("webp", "jpeg", "jpg", "png", "jpe"); + List<String> VALID_CRYPTO_EXTENSIONS = Arrays.asList("pgp", "gpg", "otr"); + List<String> WELL_KNOWN_EXTENSIONS = Arrays.asList("pdf","m4a","mp4","3gp","aac","amr","mp3"); + + 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/thedevstack/conversationsplus/entities/TransferablePlaceholder.java b/src/main/java/de/thedevstack/conversationsplus/entities/TransferablePlaceholder.java new file mode 100644 index 00000000..c51320d1 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/TransferablePlaceholder.java @@ -0,0 +1,34 @@ +package de.thedevstack.conversationsplus.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() { + + } +} |