diff options
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus/entities')
14 files changed, 3447 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..1b6539da --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java @@ -0,0 +1,411 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; +import android.os.SystemClock; + +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.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CopyOnWriteArraySet; + +import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.OtrEngine; +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 PINNED_MECHANISM_KEY = "pinned_mechanism"; + + public static final int OPTION_USETLS = 0; + public static final int OPTION_DISABLED = 1; + public static final int OPTION_REGISTER = 2; + public static final int OPTION_USECOMPRESSION = 3; + + public static 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); + + 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; + default: + return R.string.account_status_unknown; + } + } + } + + public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>(); + public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>(); + 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 boolean online = false; + private OtrEngine otrEngine = 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); + } + + public Account(final String uuid, final Jid jid, + final String password, final int options, final String rosterVersion, final String keys, + final String avatar) { + 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; + } + + 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))); + } + + 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 setUsername(final String username) throws InvalidJidException { + jid = Jid.fromParts(username, jid.getDomainpart(), jid.getResourcepart()); + } + + public Jid getServer() { + return jid.toDomainJid(); + } + + public void setServer(final String server) throws InvalidJidException { + jid = Jid.fromParts(jid.getLocalpart(), server, jid.getResourcepart()); + } + + public String getPassword() { + return password; + } + + public void setPassword(final String password) { + this.password = password; + } + + 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() >= 2; + } + + public String getResource() { + return jid.getResourcepart(); + } + + public void setResource(final String resource) { + try { + jid = Jid.fromParts(jid.getLocalpart(), jid.getDomainpart(), resource); + } catch (final InvalidJidException ignored) { + } + } + + public Jid getJid() { + return jid; + } + + public JSONObject getKeys() { + return keys; + } + + public boolean setKey(final String keyName, final String keyValue) { + try { + this.keys.put(keyName, keyValue); + return true; + } catch (final JSONException e) { + return false; + } + } + + @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); + return values; + } + + public void initOtrEngine(final XmppConnectionService context) { + this.otrEngine = new OtrEngine(context, this); + } + + public OtrEngine getOtrEngine() { + return this.otrEngine; + } + + 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.otrEngine == null) { + return null; + } + final PublicKey publicKey = this.otrEngine.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() { + if (keys.has("pgp_signature")) { + try { + return keys.getString("pgp_signature"); + } catch (final JSONException e) { + return null; + } + } else { + return null; + } + } + + 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..ea7df2f2 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java @@ -0,0 +1,160 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +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 (getName() != null) { + return getName(); + } else { + return this.getJid().getLocalpart(); + } + } + + @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; + } + + public String getNick() { + Element nick = this.findChild("nick"); + if (nick != null) { + return nick.getContent(); + } else { + return null; + } + } + + 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() { + Element password = this.findChild("password"); + if (password != null) { + return password.getContent(); + } else { + return null; + } + } + + 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 getName() { + return this.getAttribute("name"); + } + + public void setName(String name) { + this.name = name; + } + + 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..07d42d28 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java @@ -0,0 +1,505 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import de.thedevstack.conversationsplus.utils.UIHelper; +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +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 Jid jid; + protected int subscription = 0; + protected String systemAccount; + protected String photoUri; + protected String avatar; + protected JSONObject keys = new JSONObject(); + protected JSONArray groups = new JSONArray(); + protected Presences presences = new Presences(); + protected Account account; + + 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(); + } + this.avatar = avatar; + 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.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(); + } + } + + 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 Presences.CHAT: + case Presences.ONLINE: + tags.add(new Tag("online", 0xff259b24)); + break; + case Presences.AWAY: + tags.add(new Tag("away", 0xffff9800)); + break; + case Presences.XA: + tags.add(new Tag("not available", 0xffe51c23)); + break; + case Presences.DND: + tags.add(new Tag("dnd", 0xffe51c23)); + break; + } + if (isBlocked()) { + tags.add(new Tag("blocked", 0xff2e2f3b)); + } + return tags; + } + + public boolean match(String needle) { + if (needle == null || needle.isEmpty()) { + return true; + } + needle = needle.toLowerCase(Locale.US).trim(); + String[] parts = needle.split("\\s+"); + if (parts.length > 1) { + for(int i = 0; i < parts.length; ++i) { + if (!match(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() { + 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); + 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 setPresences(Presences pres) { + this.presences = pres; + } + + public void updatePresence(final String resource, final int status) { + this.presences.updatePresence(resource, status); + } + + public void removePresence(final String resource) { + this.presences.removePresence(resource); + } + + public void clearPresences() { + this.presences.clearPresences(); + this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); + } + + public int getMostAvailableStatus() { + return this.presences.getMostAvailableStatus(); + } + + public void setPhotoUri(String uri) { + this.photoUri = uri; + } + + 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() { + 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) { + 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() { + 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) { + 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); + break; + case "both": + this.setOption(Options.TO); + this.setOption(Options.FROM); + this.resetOption(Options.PREEMPTIVE_GRANT); + 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(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 boolean deleteOtrFingerprint(String fingerprint) { + 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 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..37cbff46 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java @@ -0,0 +1,770 @@ +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.JSONException; +import org.json.JSONObject; + +import java.security.interfaces.DSAPublicKey; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import de.thedevstack.conversationsplus.Config; +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_LAST_MESSAGE_TRANSMITTED = "last_message_transmitted"; + + 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; + + 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 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 findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for (Message message : this.messages) { + if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) + && (message.getEncryption() == Message.ENCRYPTION_OTR)) { + 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 findSentMessageWithUuid(String uuid) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (uuid.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND && uuid + .equals(message.getRemoteMsgId()))) { + return message; + } + } + } + return null; + } + + public void populateWithMessages(final List<Message> messages) { + synchronized (this.messages) { + messages.clear(); + messages.addAll(this.messages); + } + } + + @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 interface OnMessageFound { + public 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 void markRead() { + for (int i = this.messages.size() - 1; i >= 0; --i) { + if (messages.get(i).isRead()) { + break; + } + this.messages.get(i).markRead(); + } + } + + 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.getName() != null) { + return bookmark.getName(); + } 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().getOtrEngine()); + 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 void startOtrIfNeeded() { + if (this.otrSession != null + && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { + try { + this.otrSession.startSession(); + } catch (OtrException e) { + this.resetOtrSession(); + } + } + } + + 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().getOtrEngine().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()); + } + + 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; + } + + public int getLatestEncryption() { + int latestEncryption = this.getLatestMessage().getEncryption(); + if ((latestEncryption == Message.ENCRYPTION_DECRYPTED) + || (latestEncryption == Message.ENCRYPTION_DECRYPTION_FAILED)) { + return Message.ENCRYPTION_PGP; + } else { + return latestEncryption; + } + } + + public int getNextEncryption(boolean force) { + int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); + if (next == -1) { + int latest = this.getLatestEncryption(); + if (latest == Message.ENCRYPTION_NONE) { + if (force && getMode() == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else if (getContact().getPresences().size() == 1) { + if (getContact().getOtrFingerprints().size() >= 1) { + return Message.ENCRYPTION_OTR; + } else { + return latest; + } + } else { + return latest; + } + } else { + return latest; + } + } + if (next == Message.ENCRYPTION_NONE && force + && getMode() == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else { + 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) && message.getBody() != null && message.getBody().equals(body)) { + return message; + } + } + return null; + } + } + + public boolean setLastMessageTransmitted(long value) { + long before = getLastMessageTransmitted(); + if (value - before > 1000) { + this.setAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED, String.valueOf(value)); + return true; + } else { + return false; + } + } + + public long getLastMessageTransmitted() { + long timestamp = getLongAttribute(ATTRIBUTE_LAST_MESSAGE_TRANSMITTED,0); + if (timestamp == 0) { + 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) { + return message.getTimeSent(); + } + } + } + } + return timestamp; + } + + 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 setAttribute(String key, String value) { + try { + this.attributes.put(key, value); + return true; + } catch (JSONException e) { + return false; + } + } + + public String getAttribute(String key) { + try { + return this.attributes.getString(key); + } catch (JSONException e) { + return null; + } + } + + 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 void add(Message message) { + message.setConversation(this); + synchronized (this.messages) { + this.messages.add(message); + } + } + + public void addAll(int index, List<Message> messages) { + synchronized (this.messages) { + this.messages.addAll(index, 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() { + synchronized (this.messages) { + int count = 0; + for(int i = this.messages.size() - 1; i >= 0; --i) { + if (this.messages.get(i).isRead()) { + return count; + } + ++count; + } + return count; + } + } + + public class Smp { + public static final int STATUS_NONE = 0; + public static final int STATUS_CONTACT_REQUESTED = 1; + public static final int STATUS_WE_REQUESTED = 2; + public static final int STATUS_FAILED = 3; + public static final int STATUS_VERIFIED = 4; + + public String secret = null; + public String hint = null; + public int status = 0; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Downloadable.java b/src/main/java/de/thedevstack/conversationsplus/entities/Downloadable.java new file mode 100644 index 00000000..20169dc8 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Downloadable.java @@ -0,0 +1,28 @@ +package de.thedevstack.conversationsplus.entities; + +public interface Downloadable { + + public final String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"}; + public final String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"}; + + public static final int STATUS_UNKNOWN = 0x200; + public static final int STATUS_CHECKING = 0x201; + public static final int STATUS_FAILED = 0x202; + public static final int STATUS_OFFER = 0x203; + public static final int STATUS_DOWNLOADING = 0x204; + public static final int STATUS_DELETED = 0x205; + public static final int STATUS_OFFER_CHECK_FILESIZE = 0x206; + public static final int STATUS_UPLOADING = 0x207; + + public boolean start(); + + public int getStatus(); + + public long getFileSize(); + + public int getProgress(); + + public String getMimeType(); + + public void cancel(); +} 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..7566c199 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java @@ -0,0 +1,172 @@ +package de.thedevstack.conversationsplus.entities; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URLConnection; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import de.thedevstack.conversationsplus.Config; +import android.util.Log; + +public class DownloadableFile extends File { + + private static final long serialVersionUID = 2247012619505115863L; + + private long expectedSize = 0; + private String sha1sum; + private Key aeskey; + private String mime; + + 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() { + if (this.aeskey != null) { + if (this.expectedSize == 0) { + return 0; + } else { + return (this.expectedSize / 16 + 1) * 16; + } + } else { + return this.expectedSize; + } + } + + public String getMimeType() { + String path = this.getAbsolutePath(); + try { + String mime = URLConnection.guessContentTypeFromName(path.replace("#","")); + if (mime != null) { + return mime; + } else if (mime == null && path.endsWith(".webp")) { + return "image/webp"; + } else { + return ""; + } + } catch (final StringIndexOutOfBoundsException e) { + 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 setKey(byte[] key) { + if (key.length == 48) { + byte[] secretKey = new byte[32]; + byte[] iv = new byte[16]; + System.arraycopy(key, 0, iv, 0, 16); + System.arraycopy(key, 16, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + this.iv = iv; + } else if (key.length >= 32) { + byte[] secretKey = new byte[32]; + System.arraycopy(key, 0, secretKey, 0, 32); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } else if (key.length >= 16) { + byte[] secretKey = new byte[16]; + System.arraycopy(key, 0, secretKey, 0, 16); + this.aeskey = new SecretKeySpec(secretKey, "AES"); + } + } + + public Key getKey() { + return this.aeskey; + } + + public InputStream createInputStream() { + if (this.getKey() == null) { + try { + return new FileInputStream(this); + } catch (FileNotFoundException e) { + return null; + } + } else { + try { + IvParameterSpec ips = new IvParameterSpec(iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, this.getKey(), ips); + Log.d(Config.LOGTAG, "opening encrypted input stream"); + return new CipherInputStream(new FileInputStream(this), cipher); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); + return null; + } catch (FileNotFoundException e) { + return null; + } + } + } + + public OutputStream createOutputStream() { + if (this.getKey() == null) { + try { + return new FileOutputStream(this); + } catch (FileNotFoundException e) { + return null; + } + } else { + try { + IvParameterSpec ips = new IvParameterSpec(this.iv); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, this.getKey(), ips); + Log.d(Config.LOGTAG, "opening encrypted output stream"); + return new CipherOutputStream(new FileOutputStream(this), + cipher); + } catch (NoSuchAlgorithmException e) { + Log.d(Config.LOGTAG, "no such algo: " + e.getMessage()); + return null; + } catch (NoSuchPaddingException e) { + Log.d(Config.LOGTAG, "no such padding: " + e.getMessage()); + return null; + } catch (InvalidKeyException e) { + Log.d(Config.LOGTAG, "invalid key: " + e.getMessage()); + return null; + } catch (InvalidAlgorithmParameterException e) { + Log.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); + return null; + } catch (FileNotFoundException e) { + return null; + } + } + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/DownloadablePlaceholder.java b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadablePlaceholder.java new file mode 100644 index 00000000..741d2990 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadablePlaceholder.java @@ -0,0 +1,39 @@ +package de.thedevstack.conversationsplus.entities; + +public class DownloadablePlaceholder implements Downloadable { + + private int status; + + public DownloadablePlaceholder(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 String getMimeType() { + return ""; + } + + @Override + public void cancel() { + + } +} 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..825eb481 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java @@ -0,0 +1,33 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.List; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public interface ListItem extends Comparable<ListItem> { + public String getDisplayName(); + + public Jid getJid(); + + public List<Tag> getTags(); + + public 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; + } + } + + public 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..cd7556b3 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java @@ -0,0 +1,587 @@ +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.utils.GeoHelper; +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 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 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 REMOTE_MSG_ID = "remoteMsgId"; + public static final String SERVER_MSG_ID = "serverMsgId"; + public static final String RELATIVE_FILE_PATH = "relativeFilePath"; + 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 String relativeFilePath; + protected boolean read = true; + protected String remoteMsgId = null; + protected String serverMsgId = null; + protected Conversation conversation = null; + protected Downloadable downloadable = null; + private Message mNextMessage = null; + private Message mPreviousMessage = 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, + null, + null, + null); + 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 String remoteMsgId, + final String relativeFilePath, final String serverMsgId) { + 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.remoteMsgId = remoteMsgId; + this.relativeFilePath = relativeFilePath; + this.serverMsgId = serverMsgId; + } + + 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.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), + cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); + } + + public static Message createStatusMessage(Conversation conversation, String body) { + 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(REMOTE_MSG_ID, remoteMsgId); + values.put(RELATIVE_FILE_PATH, relativeFilePath); + values.put(SERVER_MSG_ID,serverMsgId); + 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 void setTrueCounterpart(Jid trueCounterpart) { + this.trueCounterpart = trueCounterpart; + } + + public Downloadable getDownloadable() { + return this.downloadable; + } + + public void setDownloadable(Downloadable downloadable) { + this.downloadable = downloadable; + } + + public boolean equals(Message message) { + if (this.serverMsgId != null && message.getServerMsgId() != null) { + return this.serverMsgId.equals(message.getServerMsgId()); + } else if (this.body == null || this.counterpart == null) { + return false; + } else if (message.getRemoteMsgId() != null) { + return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) + && this.counterpart.equals(message.getCounterpart()) + && this.body.equals(message.getBody()); + } else { + return this.remoteMsgId == null + && this.counterpart.equals(message.getCounterpart()) + && this.body.equals(message.getBody()) + && Math.abs(this.getTimeSent() - message.getTimeSent()) < Config.PING_TIMEOUT * 500; + } + } + + 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 mergeable(final Message message) { + return message != null && + (message.getType() == Message.TYPE_TEXT && + this.getDownloadable() == null && + message.getDownloadable() == null && + message.getEncryption() != Message.ENCRYPTION_PGP && + 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()) && + (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && + !GeoHelper.isGeoUri(message.getBody()) && + !GeoHelper.isGeoUri(this.body) && + !message.bodyContainsDownloadable() && + !this.bodyContainsDownloadable() && + !message.getBody().startsWith(ME_COMMAND) && + !this.getBody().startsWith(ME_COMMAND) + ); + } + + 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() { + final Message next = this.next(); + if (this.mergeable(next)) { + return getBody() + '\n' + next.getMergedBody(); + } + return getBody(); + } + + public boolean hasMeCommand() { + return getMergedBody().startsWith(ME_COMMAND); + } + + public int getMergedStatus() { + final Message next = this.next(); + if (this.mergeable(next)) { + return next.getStatus(); + } + return getStatus(); + } + + public long getMergedTimeSent() { + Message next = this.next(); + if (this.mergeable(next)) { + return next.getMergedTimeSent(); + } else { + return getTimeSent(); + } + } + + 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 bodyContainsDownloadable() { + /** + * there are a few cases where spaces result in an unwanted behavior, e.g. + * "http://upload.mitsu-freunde-bw.de/uploads/2015/03/i43b4bpr8.png /abc.png" + * or more than one image link in one message. + */ + if (body.contains(" ")) { + return false; + } + try { + URL url = new URL(body); + if (!url.getProtocol().equalsIgnoreCase("http") + && !url.getProtocol().equalsIgnoreCase("https")) { + return false; + } + + String sUrlPath = url.getPath(); + if (sUrlPath == null || sUrlPath.isEmpty()) { + return false; + } + + int iSlashIndex = sUrlPath.lastIndexOf('/') + 1; + + String sLastUrlPath = sUrlPath.substring(iSlashIndex).toLowerCase(); + + String[] extensionParts = sLastUrlPath.split("\\."); + if (extensionParts.length == 2 + && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains( + extensionParts[extensionParts.length - 1])) { + return true; + } else if (extensionParts.length == 3 + && Arrays + .asList(Downloadable.VALID_CRYPTO_EXTENSIONS) + .contains(extensionParts[extensionParts.length - 1]) + && Arrays.asList(Downloadable.VALID_IMAGE_EXTENSIONS).contains( + extensionParts[extensionParts.length - 2])) { + return true; + } else { + return false; + } + } catch (MalformedURLException e) { + return false; + } + } + + public ImageParams getImageParams() { + ImageParams params = getLegacyImageParams(); + if (params != null) { + return params; + } + params = new ImageParams(); + if (this.downloadable != null) { + params.size = this.downloadable.getFileSize(); + } + if (body == null) { + return params; + } + String parts[] = body.split("\\|"); + if (parts.length == 1) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + params.origin = parts[0]; + try { + params.url = new URL(parts[0]); + } catch (MalformedURLException e1) { + params.url = null; + } + } + } else if (parts.length == 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; + } + } else if (parts.length == 4) { + params.origin = parts[0]; + 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 e) { + params.width = 0; + } + try { + params.height = Integer.parseInt(parts[3]); + } catch (NumberFormatException e) { + params.height = 0; + } + } + return params; + } + + public ImageParams getLegacyImageParams() { + ImageParams params = new ImageParams(); + if (body == null) { + return params; + } + String parts[] = body.split(","); + if (parts.length == 3) { + try { + params.size = Long.parseLong(parts[0]); + } catch (NumberFormatException e) { + return null; + } + try { + params.width = Integer.parseInt(parts[1]); + } catch (NumberFormatException e) { + return null; + } + try { + params.height = Integer.parseInt(parts[2]); + } catch (NumberFormatException e) { + return null; + } + return params; + } else { + return null; + } + } + + public void untie() { + this.mNextMessage = null; + this.mPreviousMessage = null; + } + + public boolean isFileOrImage() { + return type == TYPE_FILE || type == TYPE_IMAGE; + } + + public class ImageParams { + public URL url; + public long size = 0; + public int width = 0; + public int height = 0; + public String origin; + } +} 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..860643e0 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java @@ -0,0 +1,530 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.thedevstack.conversationsplus.R; +import de.thedevstack.conversationsplus.crypto.PgpEngine; +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.stanzas.PresencePacket; + +import android.annotation.SuppressLint; + +@SuppressLint("DefaultLocale") +public class MucOptions { + + 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); + + private 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), + VISITOR("visitor", R.string.visitor), + PARTICIPANT("participant", R.string.participant), + NONE("none", R.string.no_role); + + private Role(String string, int resId) { + this.string = string; + this.resId = resId; + } + + private String string; + private int resId; + + public int getResId() { + return resId; + } + + @Override + public String toString() { + return this.string; + } + } + + public static final int ERROR_NO_ERROR = 0; + public static final int ERROR_NICK_IN_USE = 1; + public static final int ERROR_UNKNOWN = 2; + public static final int ERROR_PASSWORD_REQUIRED = 3; + public static final int ERROR_BANNED = 4; + public static final int ERROR_MEMBERS_ONLY = 5; + + public static final int KICKED_FROM_ROOM = 9; + + 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_LOST_MEMBERSHIP = "321"; + + private interface OnEventListener { + public void onSuccess(); + + public void onFailure(); + } + + public interface OnRenameListener extends OnEventListener { + + } + + public interface OnJoinListener extends OnEventListener { + + } + + public class User { + private Role role = Role.NONE; + private Affiliation affiliation = Affiliation.NONE; + private String name; + private Jid jid; + private long pgpKeyId = 0; + + public String getName() { + return name; + } + + public void setName(String user) { + this.name = user; + } + + 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 name != null && name.equals(o.name) + && 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 account.getRoster().getContactFromRoster(getJid()); + } + } + + private Account account; + private List<User> users = new CopyOnWriteArrayList<>(); + private List<String> features = new ArrayList<>(); + private Conversation conversation; + private boolean isOnline = false; + private int error = ERROR_UNKNOWN; + private OnRenameListener onRenameListener = null; + private OnJoinListener onJoinListener = null; + private User self = new User(); + private String subject = null; + private String password = null; + private boolean mNickChangingInProgress = false; + + public MucOptions(Conversation conversation) { + this.account = conversation.getAccount(); + this.conversation = conversation; + } + + public void updateFeatures(ArrayList<String> features) { + this.features.clear(); + this.features.addAll(features); + } + + public boolean hasFeature(String feature) { + return this.features.contains(feature); + } + + public boolean canInvite() { + return !membersOnly() || self.getAffiliation().ranks(Affiliation.ADMIN); + } + + public boolean membersOnly() { + return hasFeature("muc_membersonly"); + } + + public boolean nonanonymous() { + return hasFeature("muc_nonanonymous"); + } + + public boolean persistent() { + return hasFeature("muc_persistent"); + } + + public void deleteUser(String name) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(name)) { + users.remove(i); + return; + } + } + } + + public void addUser(User user) { + for (int i = 0; i < users.size(); ++i) { + if (users.get(i).getName().equals(user.getName())) { + users.set(i, user); + return; + } + } + users.add(user); + } + + public void processPacket(PresencePacket packet, PgpEngine pgp) { + final Jid from = packet.getFrom(); + if (!from.isBareJid()) { + final String name = from.getResourcepart(); + final String type = packet.getAttribute("type"); + final Element x = packet.findChild("x", "http://jabber.org/protocol/muc#user"); + final List<String> codes = getStatusCodes(x); + if (type == null) { + User user = new User(); + if (x != null) { + Element item = x.findChild("item"); + if (item != null && name != null) { + user.setName(name); + user.setAffiliation(item.getAttribute("affiliation")); + user.setRole(item.getAttribute("role")); + user.setJid(item.getAttributeAsJid("jid")); + if (codes.contains(STATUS_CODE_SELF_PRESENCE) || packet.getFrom().equals(this.conversation.getJid())) { + this.isOnline = true; + this.error = ERROR_NO_ERROR; + self = user; + if (mNickChangingInProgress) { + onRenameListener.onSuccess(); + mNickChangingInProgress = false; + } else if (this.onJoinListener != null) { + this.onJoinListener.onSuccess(); + this.onJoinListener = null; + } + } else { + addUser(user); + } + if (pgp != null) { + Element signed = packet.findChild("x", "jabber:x:signed"); + if (signed != null) { + Element status = packet.findChild("status"); + String msg; + if (status != null) { + msg = status.getContent(); + } else { + msg = ""; + } + user.setPgpKeyId(pgp.fetchKeyId(account, msg, + signed.getContent())); + } + } + } + } + } else if (type.equals("unavailable")) { + if (codes.contains(STATUS_CODE_SELF_PRESENCE) || + packet.getFrom().equals(this.conversation.getJid())) { + if (codes.contains(STATUS_CODE_CHANGED_NICK)) { + this.mNickChangingInProgress = true; + } else if (codes.contains(STATUS_CODE_KICKED)) { + setError(KICKED_FROM_ROOM); + } else if (codes.contains(STATUS_CODE_BANNED)) { + setError(ERROR_BANNED); + } else if (codes.contains(STATUS_CODE_LOST_MEMBERSHIP)) { + setError(ERROR_MEMBERS_ONLY); + } else { + setError(ERROR_UNKNOWN); + } + } else { + deleteUser(name); + } + } else if (type.equals("error")) { + Element error = packet.findChild("error"); + if (error != null && error.hasChild("conflict")) { + if (isOnline) { + if (onRenameListener != null) { + onRenameListener.onFailure(); + } + } else { + setError(ERROR_NICK_IN_USE); + } + } else if (error != null && error.hasChild("not-authorized")) { + setError(ERROR_PASSWORD_REQUIRED); + } else if (error != null && error.hasChild("forbidden")) { + setError(ERROR_BANNED); + } else if (error != null && error.hasChild("registration-required")) { + setError(ERROR_MEMBERS_ONLY); + } else { + setError(ERROR_UNKNOWN); + } + } + } + } + + private void setError(int error) { + this.isOnline = false; + this.error = error; + if (onJoinListener != null) { + onJoinListener.onFailure(); + onJoinListener = null; + } + } + + private List<String> getStatusCodes(Element x) { + List<String> codes = new ArrayList<>(); + if (x != null) { + for (Element child : x.getChildren()) { + if (child.getName().equals("status")) { + String code = child.getAttribute("code"); + if (code != null) { + codes.add(code); + } + } + } + } + return codes; + } + + public List<User> getUsers() { + return this.users; + } + + 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 int getError() { + return this.error; + } + + public void setOnRenameListener(OnRenameListener listener) { + this.onRenameListener = listener; + } + + public void setOnJoinListener(OnJoinListener listener) { + this.onJoinListener = listener; + } + + public void setOffline() { + this.users.clear(); + this.error = 0; + 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<String>(); + for (User user : users) { + 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 : getUsers()) { + if (user.getPgpKeyId() != 0) { + ids.add(user.getPgpKeyId()); + } + } + long[] primitivLongArray = new long[ids.size()]; + for (int i = 0; i < ids.size(); ++i) { + primitivLongArray[i] = ids.get(i); + } + return primitivLongArray; + } + + public boolean pgpKeysInUse() { + for (User user : getUsers()) { + if (user.getPgpKeyId() != 0) { + return true; + } + } + return false; + } + + public boolean everybodyHasKeys() { + for (User user : getUsers()) { + 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 counterpart) { + for (User user : this.getUsers()) { + if (user.getName().equals(counterpart)) { + return user.getJid(); + } + } + return null; + } + + 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; + } +} 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..cb984648 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java @@ -0,0 +1,90 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Map.Entry; + +import de.thedevstack.conversationsplus.xml.Element; + +public class Presences { + + public static final int CHAT = -1; + public static final int ONLINE = 0; + public static final int AWAY = 1; + public static final int XA = 2; + public static final int DND = 3; + public static final int OFFLINE = 4; + + private Hashtable<String, Integer> presences = new Hashtable<String, Integer>(); + + public Hashtable<String, Integer> getPresences() { + return this.presences; + } + + public void updatePresence(String resource, int status) { + synchronized (this.presences) { + this.presences.put(resource, status); + } + } + + public void removePresence(String resource) { + synchronized (this.presences) { + this.presences.remove(resource); + } + } + + public void clearPresences() { + synchronized (this.presences) { + this.presences.clear(); + } + } + + public int getMostAvailableStatus() { + int status = OFFLINE; + synchronized (this.presences) { + Iterator<Entry<String, Integer>> it = presences.entrySet().iterator(); + while (it.hasNext()) { + Entry<String, Integer> entry = it.next(); + if (entry.getValue() < status) + status = entry.getValue(); + } + } + return status; + } + + public static int parseShow(Element show) { + if ((show == null) || (show.getContent() == null)) { + return Presences.ONLINE; + } else if (show.getContent().equals("away")) { + return Presences.AWAY; + } else if (show.getContent().equals("xa")) { + return Presences.XA; + } else if (show.getContent().equals("chat")) { + return Presences.CHAT; + } else if (show.getContent().equals("dnd")) { + return Presences.DND; + } else { + return Presences.OFFLINE; + } + } + + 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..0c719ed9 --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Roster.java @@ -0,0 +1,91 @@ +package de.thedevstack.conversationsplus.entities; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import de.thedevstack.conversationsplus.xmpp.jid.Jid; + +public class Roster { + final Account account; + final HashMap<String, 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().toString()); + if (contact != null && contact.showInRoster()) { + return contact; + } else { + return null; + } + } + } + + public Contact getContact(final Jid jid) { + synchronized (this.contacts) { + final Jid bareJid = jid.toBareJid(); + if (contacts.containsKey(bareJid.toString())) { + return contacts.get(bareJid.toString()); + } else { + Contact contact = new Contact(bareJid); + contact.setAccount(account); + contacts.put(bareJid.toString(), contact); + return contact; + } + } + } + + public void clearPresences() { + for (Contact contact : getContacts()) { + contact.clearPresences(); + } + } + + public void markAllAsNotInRoster() { + for (Contact contact : getContacts()) { + contact.resetOption(Contact.Options.IN_ROSTER); + } + } + + public void clearSystemAccounts() { + for (Contact contact : getContacts()) { + contact.setPhotoUri(null); + contact.setSystemName(null); + contact.setSystemAccount(null); + } + } + + public List<Contact> getContacts() { + synchronized (this.contacts) { + return new ArrayList<>(this.contacts.values()); + } + } + + public void initContact(final Contact contact) { + contact.setAccount(account); + contact.setOption(Contact.Options.IN_ROSTER); + synchronized (this.contacts) { + contacts.put(contact.getJid().toBareJid().toString(), contact); + } + } + + public void setVersion(String version) { + this.version = version; + } + + public String getVersion() { + return this.version; + } + + public Account getAccount() { + return this.account; + } +} |