diff options
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus/entities')
12 files changed, 1166 insertions, 543 deletions
diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Account.java b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java index 8d879fa0..f7dee013 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Account.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Account.java @@ -3,6 +3,7 @@ package de.thedevstack.conversationsplus.entities; import android.content.ContentValues; import android.database.Cursor; import android.os.SystemClock; +import android.util.Pair; import net.java.otr4j.crypto.OtrCryptoEngineImpl; import net.java.otr4j.crypto.OtrCryptoException; @@ -13,13 +14,19 @@ import org.json.JSONObject; import java.security.PublicKey; import java.security.interfaces.DSAPublicKey; import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import de.thedevstack.conversationsplus.ConversationsPlusPreferences; import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.R; import de.thedevstack.conversationsplus.crypto.OtrService; +import de.thedevstack.conversationsplus.crypto.PgpDecryptionService; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceImpl; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlServiceStub; import de.thedevstack.conversationsplus.services.XmppConnectionService; import de.thedevstack.conversationsplus.xmpp.XmppConnection; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; @@ -36,6 +43,9 @@ public class Account extends AbstractEntity { public static final String ROSTERVERSION = "rosterversion"; public static final String KEYS = "keys"; public static final String AVATAR = "avatar"; + public static final String DISPLAY_NAME = "display_name"; + public static final String HOSTNAME = "hostname"; + public static final String PORT = "port"; public static final String PINNED_MECHANISM_KEY = "pinned_mechanism"; @@ -43,12 +53,29 @@ public class Account extends AbstractEntity { public static final int OPTION_DISABLED = 1; public static final int OPTION_REGISTER = 2; public static final int OPTION_USECOMPRESSION = 3; + public final HashSet<Pair<String, String>> inProgressDiscoFetches = new HashSet<>(); public boolean httpUploadAvailable() { return xmppConnection != null && xmppConnection.getFeatures().httpUpload(); } - public static enum State { + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + public XmppConnection.Identity getServerIdentity() { + if (xmppConnection == null) { + return XmppConnection.Identity.UNKNOWN; + } else { + return xmppConnection.getServerIdentity(); + } + } + + public enum State { DISABLED, OFFLINE, CONNECTING, @@ -61,7 +88,8 @@ public class Account extends AbstractEntity { REGISTRATION_SUCCESSFUL, REGISTRATION_NOT_SUPPORTED(true), SECURITY_ERROR(true), - INCOMPATIBLE_SERVER(true); + INCOMPATIBLE_SERVER(true), + TOR_NOT_AVAILABLE(true); private final boolean isError; @@ -105,6 +133,8 @@ public class Account extends AbstractEntity { return R.string.account_status_security_error; case INCOMPATIBLE_SERVER: return R.string.account_status_incompatible_server; + case TOR_NOT_AVAILABLE: + return R.string.account_status_tor_unavailable; default: return R.string.account_status_unknown; } @@ -113,6 +143,10 @@ public class Account extends AbstractEntity { public List<Conversation> pendingConferenceJoins = new CopyOnWriteArrayList<>(); public List<Conversation> pendingConferenceLeaves = new CopyOnWriteArrayList<>(); + + private static final String KEY_PGP_SIGNATURE = "pgp_signature"; + private static final String KEY_PGP_ID = "pgp_id"; + protected Jid jid; protected String password; protected int options = 0; @@ -120,15 +154,19 @@ public class Account extends AbstractEntity { protected State status = State.OFFLINE; protected JSONObject keys = new JSONObject(); protected String avatar; + protected String displayName = null; + protected String hostname = null; + protected int port = 5222; protected boolean online = false; private OtrService mOtrService = null; + private AxolotlService axolotlService = null; + private PgpDecryptionService pgpDecryptionService = null; private XmppConnection xmppConnection = null; private long mEndGracePeriod = 0L; private String otrFingerprint; private final Roster roster = new Roster(this); private List<Bookmark> bookmarks = new CopyOnWriteArrayList<>(); private final Collection<Jid> blocklist = new CopyOnWriteArraySet<>(); - private XmppConnectionService mXmppConnectionService; public Account() { this.uuid = "0"; @@ -136,12 +174,12 @@ public class Account extends AbstractEntity { public Account(final Jid jid, final String password) { this(java.util.UUID.randomUUID().toString(), jid, - password, 0, null, "", null); + password, 0, null, "", null, null, null, 5222); } - public Account(final String uuid, final Jid jid, + private Account(final String uuid, final Jid jid, final String password, final int options, final String rosterVersion, final String keys, - final String avatar) { + final String avatar, String displayName, String hostname, int port) { this.uuid = uuid; this.jid = jid; if (jid.isBareJid()) { @@ -156,6 +194,9 @@ public class Account extends AbstractEntity { this.keys = new JSONObject(); } this.avatar = avatar; + this.displayName = displayName; + this.hostname = hostname; + this.port = port; } public static Account fromCursor(final Cursor cursor) { @@ -171,7 +212,10 @@ public class Account extends AbstractEntity { cursor.getInt(cursor.getColumnIndex(OPTIONS)), cursor.getString(cursor.getColumnIndex(ROSTERVERSION)), cursor.getString(cursor.getColumnIndex(KEYS)), - cursor.getString(cursor.getColumnIndex(AVATAR))); + cursor.getString(cursor.getColumnIndex(AVATAR)), + cursor.getString(cursor.getColumnIndex(DISPLAY_NAME)), + cursor.getString(cursor.getColumnIndex(HOSTNAME)), + cursor.getInt(cursor.getColumnIndex(PORT))); } public boolean isOptionSet(final int option) { @@ -190,18 +234,14 @@ public class Account extends AbstractEntity { return jid.getLocalpart(); } - public void setUsername(final String username) throws InvalidJidException { - jid = Jid.fromParts(username, jid.getDomainpart(), jid.getResourcepart()); + public void setJid(final Jid jid) { + this.jid = jid; } 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; } @@ -210,6 +250,22 @@ public class Account extends AbstractEntity { this.password = password; } + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getHostname() { + return this.hostname == null ? "" : this.hostname; + } + + public void setPort(int port) { + this.port = port; + } + + public int getPort() { + return this.port; + } + public State getStatus() { if (isOptionSet(OPTION_DISABLED)) { return State.DISABLED; @@ -227,7 +283,7 @@ public class Account extends AbstractEntity { } public boolean hasErrorStatus() { - return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 2; + return getXmppConnection() != null && getStatus().isError() && getXmppConnection().getAttempt() >= 3; } public String getResource() { @@ -255,6 +311,10 @@ public class Account extends AbstractEntity { return keys; } + public String getKey(final String name) { + return this.keys.optString(name, null); + } + public boolean setKey(final String keyName, final String keyValue) { try { this.keys.put(keyName, keyValue); @@ -264,6 +324,14 @@ public class Account extends AbstractEntity { } } + public boolean setPrivateKeyAlias(String alias) { + return setKey("private_key_alias", alias); + } + + public String getPrivateKeyAlias() { + return getKey("private_key_alias"); + } + @Override public ContentValues getContentValues() { final ContentValues values = new ContentValues(); @@ -275,18 +343,38 @@ public class Account extends AbstractEntity { values.put(KEYS, this.keys.toString()); values.put(ROSTERVERSION, rosterVersion); values.put(AVATAR, avatar); + values.put(DISPLAY_NAME, displayName); + values.put(HOSTNAME, hostname); + values.put(PORT, port); return values; } + public AxolotlService getAxolotlService() { + return axolotlService; + } + public void initAccountServices(final XmppConnectionService context) { - this.mXmppConnectionService = context; this.mOtrService = new OtrService(context, this); + if (ConversationsPlusPreferences.omemoEnabled()) { + this.axolotlService = new AxolotlServiceImpl(this, context); + if (xmppConnection != null) { + xmppConnection.addOnAdvancedStreamFeaturesAvailableListener(axolotlService); + } + } else { + this.axolotlService = new AxolotlServiceStub(); + } + + this.pgpDecryptionService = new PgpDecryptionService(context); } public OtrService getOtrService() { return this.mOtrService; } + public PgpDecryptionService getPgpDecryptionService() { + return pgpDecryptionService; + } + public XmppConnection getXmppConnection() { return this.xmppConnection; } @@ -332,17 +420,56 @@ public class Account extends AbstractEntity { } public String getPgpSignature() { - if (keys.has("pgp_signature")) { - try { - return keys.getString("pgp_signature"); - } catch (final JSONException e) { + try { + if (keys.has(KEY_PGP_SIGNATURE) && !"null".equals(keys.getString(KEY_PGP_SIGNATURE))) { + return keys.getString(KEY_PGP_SIGNATURE); + } else { return null; } - } else { + } catch (final JSONException e) { return null; } } + public boolean setPgpSignature(String signature) { + try { + keys.put(KEY_PGP_SIGNATURE, signature); + } catch (JSONException e) { + return false; + } + return true; + } + + public boolean unsetPgpSignature() { + try { + keys.put(KEY_PGP_SIGNATURE, JSONObject.NULL); + } catch (JSONException e) { + return false; + } + return true; + } + + public long getPgpId() { + if (keys.has(KEY_PGP_ID)) { + try { + return keys.getLong(KEY_PGP_ID); + } catch (JSONException e) { + return -1; + } + } else { + return -1; + } + } + + public boolean setPgpSignId(long pgpID) { + try { + keys.put(KEY_PGP_ID, pgpID); + } catch (JSONException e) { + return false; + } + return true; + } + public Roster getRoster() { return this.roster; } @@ -420,8 +547,4 @@ public class Account extends AbstractEntity { public boolean isOnlineAndConnected() { return this.getStatus() == State.ONLINE && this.getXmppConnection() != null; } - - public XmppConnectionService getXmppConnectionService() { - return mXmppConnectionService; - } } diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java index 9e67bf2d..07a77eae 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java @@ -2,12 +2,11 @@ package de.thedevstack.conversationsplus.entities; import android.graphics.Color; -import android.graphics.Color; - import java.util.ArrayList; import java.util.List; import java.util.Locale; +import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.utils.UIHelper; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -54,14 +53,26 @@ public class Bookmark extends Element implements ListItem { if (this.mJoinedConversation != null && (this.mJoinedConversation.getMucOptions().getSubject() != null)) { return this.mJoinedConversation.getMucOptions().getSubject(); - } else if (getName() != null) { - return getName(); + } else if (getBookmarkName() != null) { + return getBookmarkName(); } else { return this.getJid().getLocalpart(); } } @Override + public String getDisplayJid() { + Jid jid = getJid(); + if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.CONFERENCE_DOMAIN_LOCK)) { + return jid.getLocalpart(); + } else if (jid != null) { + return jid.toString(); + } else { + return null; + } + } + + @Override public Jid getJid() { return this.getAttributeAsJid("jid"); } @@ -143,12 +154,18 @@ public class Bookmark extends Element implements ListItem { this.mJoinedConversation = conversation; } - public String getName() { + public String getBookmarkName() { return this.getAttribute("name"); } - public void setName(String name) { - this.name = name; + public boolean setBookmarkName(String name) { + String before = getBookmarkName(); + if (name != null && !name.equals(before)) { + this.setAttribute("name", name); + return true; + } else { + return false; + } } public void unregisterConversation() { diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java index ca734403..74d76f13 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Contact.java @@ -12,6 +12,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import de.thedevstack.conversationsplus.Config; import de.thedevstack.conversationsplus.utils.UIHelper; import de.thedevstack.conversationsplus.xml.Element; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; @@ -38,13 +39,14 @@ public class Contact implements ListItem, Blockable { protected String systemName; protected String serverName; protected String presenceName; + protected String commonName; protected Jid jid; protected int subscription = 0; protected String systemAccount; protected String photoUri; protected JSONObject keys = new JSONObject(); protected JSONArray groups = new JSONArray(); - protected Presences presences = new Presences(); + protected final Presences presences = new Presences(); protected Account account; protected Avatar avatar; @@ -105,7 +107,9 @@ public class Contact implements ListItem, Blockable { } public String getDisplayName() { - if (this.systemName != null) { + if (this.commonName != null && Config.X509_VERIFICATION) { + return this.commonName; + } else if (this.systemName != null) { return this.systemName; } else if (this.serverName != null) { return this.serverName; @@ -118,6 +122,17 @@ public class Contact implements ListItem, Blockable { } } + @Override + public String getDisplayJid() { + if (Config.LOCK_DOMAINS_IN_CONVERSATIONS && jid != null && jid.getDomainpart().equals(Config.DOMAIN_LOCK)) { + return jid.getLocalpart(); + } else if (jid != null) { + return jid.toString(); + } else { + return null; + } + } + public String getProfilePhoto() { return this.photoUri; } @@ -133,17 +148,17 @@ public class Contact implements ListItem, Blockable { tags.add(new Tag(group, UIHelper.getColorForName(group))); } switch (getMostAvailableStatus()) { - case Presences.CHAT: - case Presences.ONLINE: + case CHAT: + case ONLINE: tags.add(new Tag("online", 0xff259b24)); break; - case Presences.AWAY: + case AWAY: tags.add(new Tag("away", 0xffff9800)); break; - case Presences.XA: + case XA: tags.add(new Tag("not available", 0xfff44336)); break; - case Presences.DND: + case DND: tags.add(new Tag("dnd", 0xfff44336)); break; } @@ -189,20 +204,22 @@ public class Contact implements ListItem, Blockable { } 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 == null ? null : avatar.getFilename()); - values.put(LAST_PRESENCE, lastseen.presence); - values.put(LAST_TIME, lastseen.time); - values.put(GROUPS, groups.toString()); - return values; + synchronized (this.keys) { + final ContentValues values = new ContentValues(); + values.put(ACCOUNT, accountUuid); + values.put(SYSTEMNAME, systemName); + values.put(SERVERNAME, serverName); + values.put(JID, jid.toString()); + values.put(OPTIONS, subscription); + values.put(SYSTEMACCOUNT, systemAccount); + values.put(PHOTOURI, photoUri); + values.put(KEYS, keys.toString()); + values.put(AVATAR, avatar == null ? null : avatar.getFilename()); + values.put(LAST_PRESENCE, lastseen.presence); + values.put(LAST_TIME, lastseen.time); + values.put(GROUPS, groups.toString()); + return values; + } } public int getSubscription() { @@ -222,12 +239,8 @@ public class Contact implements ListItem, Blockable { 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 updatePresence(final String resource, final Presence presence) { + this.presences.updatePresence(resource, presence); } public void removePresence(final String resource) { @@ -239,8 +252,13 @@ public class Contact implements ListItem, Blockable { this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); } - public int getMostAvailableStatus() { - return this.presences.getMostAvailableStatus(); + public Presence.Status getMostAvailableStatus() { + Presence p = this.presences.getMostAvailablePresence(); + if (p == null) { + return Presence.Status.OFFLINE; + } + + return p.getStatus(); } public boolean setPhotoUri(String uri) { @@ -287,60 +305,65 @@ public class Contact implements ListItem, Blockable { } 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)); + synchronized (this.keys) { + final ArrayList<String> fingerprints = new ArrayList<String>(); + try { + if (this.keys.has("otr_fingerprints")) { + final JSONArray prints = this.keys.getJSONArray("otr_fingerprints"); + for (int i = 0; i < prints.length(); ++i) { + final String print = prints.isNull(i) ? null : prints.getString(i); + if (print != null && !print.isEmpty()) { + fingerprints.add(prints.getString(i)); + } } } - } - } catch (final JSONException ignored) { + } catch (final JSONException ignored) { + } + return fingerprints; } - 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"); + synchronized (this.keys) { + if (getOtrFingerprints().contains(print)) { + return false; + } + try { + JSONArray fingerprints; + if (!this.keys.has("otr_fingerprints")) { + fingerprints = new JSONArray(); + } else { + fingerprints = this.keys.getJSONArray("otr_fingerprints"); + } + fingerprints.put(print); + this.keys.put("otr_fingerprints", fingerprints); + return true; + } catch (final JSONException ignored) { + return false; } - 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) { + synchronized (this.keys) { + if (this.keys.has("pgp_keyid")) { + try { + return this.keys.getLong("pgp_keyid"); + } catch (JSONException e) { + return 0; + } + } else { return 0; } - } else { - return 0; } } public void setPgpKeyId(long keyId) { - try { - this.keys.put("pgp_keyid", keyId); - } catch (final JSONException ignored) { - + synchronized (this.keys) { + try { + this.keys.put("pgp_keyid", keyId); + } catch (final JSONException ignored) { + } } } @@ -376,11 +399,13 @@ public class Contact implements ListItem, Blockable { this.resetOption(Options.TO); this.setOption(Options.FROM); this.resetOption(Options.PREEMPTIVE_GRANT); + this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); break; case "both": this.setOption(Options.TO); this.setOption(Options.FROM); this.resetOption(Options.PREEMPTIVE_GRANT); + this.resetOption(Options.PENDING_SUBSCRIPTION_REQUEST); break; case "none": this.resetOption(Options.FROM); @@ -447,24 +472,26 @@ public class Contact implements ListItem, Blockable { } 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; + synchronized (this.keys) { + boolean success = false; + try { + if (this.keys.has("otr_fingerprints")) { + JSONArray newPrints = new JSONArray(); + JSONArray oldPrints = this.keys + .getJSONArray("otr_fingerprints"); + for (int i = 0; i < oldPrints.length(); ++i) { + if (!oldPrints.getString(i).equals(fingerprint)) { + newPrints.put(oldPrints.getString(i)); + } else { + success = true; + } } + this.keys.put("otr_fingerprints", newPrints); } - this.keys.put("otr_fingerprints", newPrints); + return success; + } catch (JSONException e) { + return false; } - return success; - } catch (JSONException e) { - return false; } } @@ -504,6 +531,10 @@ public class Contact implements ListItem, Blockable { return account.getJid().toBareJid().equals(getJid().toBareJid()); } + public void setCommonName(String cn) { + this.commonName = cn; + } + public static class Lastseen { public long time; public String presence; diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java index e26f7944..0b70d938 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java @@ -19,8 +19,9 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; +import de.thedevstack.conversationsplus.utils.MessageUtil; import de.thedevstack.conversationsplus.Config; -import de.thedevstack.conversationsplus.ConversationsPlusPreferences; +import de.thedevstack.conversationsplus.crypto.axolotl.AxolotlService; import de.thedevstack.conversationsplus.xmpp.chatstate.ChatState; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -47,7 +48,7 @@ public class Conversation extends AbstractEntity implements Blockable { 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"; + public static final String ATTRIBUTE_ALWAYS_NOTIFY = "always_notify"; private String name; private String contactUuid; @@ -81,6 +82,8 @@ public class Conversation extends AbstractEntity implements Blockable { private ChatState mOutgoingChatState = Config.DEFAULT_CHATSTATE; private ChatState mIncomingChatState = Config.DEFAULT_CHATSTATE; private String mLastReceivedOtrMessageId = null; + private String mFirstMamReference = null; + private Message correctingMessage; public boolean hasMessagesLeftOnServer() { return messagesLeftOnServer; @@ -112,6 +115,16 @@ public class Conversation extends AbstractEntity implements Blockable { } } + public void findUnreadMessages(OnMessageFound onMessageFound) { + synchronized (this.messages) { + for(Message message : this.messages) { + if (!message.isRead()) { + onMessageFound.onMessageFound(message); + } + } + } + } + public void findMessagesWithFiles(final OnMessageFound onMessageFound) { synchronized (this.messages) { for (final Message message : this.messages) { @@ -180,13 +193,13 @@ public class Conversation extends AbstractEntity implements Blockable { } } - public void findUnsentMessagesWithOtrEncryption(OnMessageFound onMessageFound) { + public void findUnsentMessagesWithEncryption(int encryptionType, OnMessageFound onMessageFound) { synchronized (this.messages) { for (Message message : this.messages) { if ((message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_WAITING) - && (message.getEncryption() == Message.ENCRYPTION_OTR)) { + && (message.getEncryption() == encryptionType)) { onMessageFound.onMessageFound(message); - } + } } } } @@ -202,14 +215,43 @@ public class Conversation extends AbstractEntity implements Blockable { } } - public Message findSentMessageWithUuid(String uuid) { + public Message findSentMessageWithUuidOrRemoteId(String id) { + synchronized (this.messages) { + for (Message message : this.messages) { + if (id.equals(message.getUuid()) + || (message.getStatus() >= Message.STATUS_SEND + && id.equals(message.getRemoteMsgId()))) { + return message; + } + } + } + return null; + } + + public Message findMessageWithRemoteIdAndCounterpart(String id, Jid counterpart, boolean received, boolean carbon) { + synchronized (this.messages) { + for(int i = this.messages.size() - 1; i >= 0; --i) { + Message message = messages.get(i); + if (counterpart.equals(message.getCounterpart()) + && ((message.getStatus() == Message.STATUS_RECEIVED) == received) + && (carbon == message.isCarbon() || received) ) { + if (id.equals(message.getRemoteMsgId())) { + return message; + } else { + return null; + } + } + } + } + return null; + } + + public Message findSentMessageWithUuid(String id) { synchronized (this.messages) { for (Message message : this.messages) { - if (uuid.equals(message.getUuid()) - || (message.getStatus() >= Message.STATUS_SEND && uuid - .equals(message.getRemoteMsgId()))) { + if (id.equals(message.getUuid())) { return message; - } + } } } return null; @@ -256,9 +298,24 @@ public class Conversation extends AbstractEntity implements Blockable { } } + public void setFirstMamReference(String reference) { + this.mFirstMamReference = reference; + } + + public String getFirstMamReference() { + return this.mFirstMamReference; + } + + public void setCorrectingMessage(Message correctingMessage) { + this.correctingMessage = correctingMessage; + } + + public Message getCorrectingMessage() { + return this.correctingMessage; + } public interface OnMessageFound { - public void onMessageFound(final Message message); + void onMessageFound(final Message message); } public Conversation(final String name, final Account account, final Jid contactJid, @@ -291,13 +348,17 @@ public class Conversation extends AbstractEntity implements Blockable { 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; + public List<Message> markRead() { + final List<Message> unread = new ArrayList<>(); + synchronized (this.messages) { + for(Message message : this.messages) { + if (!message.isRead()) { + message.markRead(); + unread.add(message); + } } - this.messages.get(i).markRead(); } + return unread; } public Message getLatestMarkableMessage() { @@ -330,8 +391,8 @@ public class Conversation extends AbstractEntity implements Blockable { if (getMode() == MODE_MULTI) { if (getMucOptions().getSubject() != null) { return getMucOptions().getSubject(); - } else if (bookmark != null && bookmark.getName() != null) { - return bookmark.getName(); + } else if (bookmark != null && bookmark.getBookmarkName() != null) { + return bookmark.getBookmarkName(); } else { String generatedName = getMucOptions().createNameFromParticipants(); if (generatedName != null) { @@ -456,15 +517,18 @@ public class Conversation extends AbstractEntity implements Blockable { return mSmp; } - public void startOtrIfNeeded() { - if (this.otrSession != null - && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { + public boolean startOtrIfNeeded() { + if (this.otrSession != null && this.otrSession.getSessionStatus() != SessionStatus.ENCRYPTED) { try { this.otrSession.startSession(); + return true; } catch (OtrException e) { this.resetOtrSession(); + return false; } - } + } else { + return true; + } } public boolean endOtrIfNeeded() { @@ -520,6 +584,13 @@ public class Conversation extends AbstractEntity implements Blockable { return getContact().getOtrFingerprints().contains(getOtrFingerprint()); } + /** + * short for is Private and Non-anonymous + */ + private boolean isPnNA() { + return mode == MODE_SINGLE || (getMucOptions().membersOnly() && getMucOptions().nonanonymous()); + } + public synchronized MucOptions getMucOptions() { if (this.mucOptions == null) { this.mucOptions = new MucOptions(this); @@ -543,42 +614,70 @@ public class Conversation extends AbstractEntity implements Blockable { 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; + private int getMostRecentlyUsedOutgoingEncryption() { + synchronized (this.messages) { + for(int i = this.messages.size() -1; i >= 0; --i) { + final Message m = this.messages.get(i); + if (!m.isCarbon() && m.getStatus() != Message.STATUS_RECEIVED) { + final int e = m.getEncryption(); + if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { + return Message.ENCRYPTION_PGP; + } else { + return e; + } + } + } } + return Message.ENCRYPTION_NONE; } - 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; + private int getMostRecentlyUsedIncomingEncryption() { + synchronized (this.messages) { + for(int i = this.messages.size() -1; i >= 0; --i) { + final Message m = this.messages.get(i); + if (m.getStatus() == Message.STATUS_RECEIVED) { + final int e = m.getEncryption(); + if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { + return Message.ENCRYPTION_PGP; } else { - return latest; + return e; } + } + } + } + return Message.ENCRYPTION_NONE; + } + + public int getNextEncryption() { + final AxolotlService axolotlService = getAccount().getAxolotlService(); + int next = this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, -1); + if (next == -1) { + if (Config.X509_VERIFICATION && mode == MODE_SINGLE) { + if (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact())) { + return Message.ENCRYPTION_AXOLOTL; } else { - return latest; + return Message.ENCRYPTION_NONE; } + } + int outgoing = this.getMostRecentlyUsedOutgoingEncryption(); + if (outgoing == Message.ENCRYPTION_NONE) { + next = this.getMostRecentlyUsedIncomingEncryption(); } else { - return latest; + next = outgoing; } } - if (next == Message.ENCRYPTION_NONE && force - && getMode() == MODE_SINGLE) { - return Message.ENCRYPTION_OTR; - } else { - return next; + if (!Config.supportUnencrypted() + && (mode == MODE_SINGLE || Config.supportOpenPgpOnly()) + && next <= 0) { + if (Config.supportOmemo() && (axolotlService != null && axolotlService.isContactAxolotlCapable(getContact()) || !Config.multipleEncryptionChoices())) { + return Message.ENCRYPTION_AXOLOTL; + } else if (Config.supportOtr()) { + return Message.ENCRYPTION_OTR; + } else if (Config.supportOpenPgp()) { + return Message.ENCRYPTION_PGP; + } } + return next; } public void setNextEncryption(int encryption) { @@ -639,37 +738,32 @@ public class Conversation extends AbstractEntity implements Blockable { 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; + if (message.getStatus() == Message.STATUS_UNSEND || message.getStatus() == Message.STATUS_SEND) { + String otherBody; + if (message.hasFileOnRemoteHost()) { + otherBody = message.getFileParams().url.toString(); + } else { + otherBody = message.body; + } + if (otherBody != null && otherBody.equals(body)) { + return message; + } } } return null; } } - public 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(); - } + synchronized (this.messages) { + for(int i = this.messages.size() - 1; i >= 0; --i) { + Message message = this.messages.get(i); + if (message.getStatus() == Message.STATUS_RECEIVED || message.isCarbon()) { + return message.getTimeSent(); } } } - return timestamp; + return 0; } public void setMutedTill(long value) { @@ -680,6 +774,10 @@ public class Conversation extends AbstractEntity implements Blockable { return System.currentTimeMillis() < this.getLongAttribute(ATTRIBUTE_MUTED_TILL, 0); } + public boolean alwaysNotify() { + return mode == MODE_SINGLE || getBooleanAttribute(ATTRIBUTE_ALWAYS_NOTIFY, Config.ALWAYS_NOTIFY_BY_DEFAULT || isPnNA()); + } + public boolean setAttribute(String key, String value) { try { this.attributes.put(key, value); @@ -723,6 +821,15 @@ public class Conversation extends AbstractEntity implements Blockable { } } + public boolean getBooleanAttribute(String key, boolean defaultValue) { + String value = this.getAttribute(key); + if (value == null) { + return defaultValue; + } else { + return Boolean.parseBoolean(value); + } + } + public void add(Message message) { message.setConversation(this); synchronized (this.messages) { @@ -730,10 +837,18 @@ public class Conversation extends AbstractEntity implements Blockable { } } + public void prepend(Message message) { + message.setConversation(this); + synchronized (this.messages) { + this.messages.add(0,message); + } + } + public void addAll(int index, List<Message> messages) { synchronized (this.messages) { this.messages.addAll(index, messages); } + account.getPgpDecryptionService().addAll(messages); } public void sort() { @@ -757,6 +872,9 @@ public class Conversation extends AbstractEntity implements Blockable { } public int unreadCount() { + if (getLongAttribute(Conversation.ATTRIBUTE_MUTED_TILL,0) == Long.MAX_VALUE) { + return 0; + } synchronized (this.messages) { int count = 0; for(int i = this.messages.size() - 1; i >= 0; --i) { @@ -764,10 +882,7 @@ public class Conversation extends AbstractEntity implements Blockable { if (message.isRead()) { return count; } - if (getMode() == Conversation.MODE_SINGLE - || ConversationsPlusPreferences.alwaysNotifyInConference() - || account.getXmppConnectionService().getNotificationService().wasHighlightedOrPrivate(message) - ) { + if (alwaysNotify() || MessageUtil.wasHighlightedOrPrivate(message)) { ++count; } } diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java index 4031e546..424d0301 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java @@ -1,26 +1,7 @@ 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.android.logcat.Logging; -import de.thedevstack.conversationsplus.Config; + import de.thedevstack.conversationsplus.utils.MimeUtils; public class DownloadableFile extends File { @@ -29,8 +10,7 @@ public class DownloadableFile extends File { private long expectedSize = 0; private String sha1sum; - private Key aeskey; - private String mime; + private byte[] aeskey; private byte[] iv = { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0xf }; @@ -44,15 +24,7 @@ public class DownloadableFile extends File { } public long getExpectedSize() { - if (this.aeskey != null) { - if (this.expectedSize == 0) { - return 0; - } else { - return (this.expectedSize / 16 + 1) * 16; - } - } else { - return this.expectedSize; - } + return this.expectedSize; } public String getMimeType() { @@ -78,91 +50,38 @@ public class DownloadableFile extends File { this.sha1sum = sum; } - public void setKey(byte[] key) { - if (key.length == 48) { + public void setKeyAndIv(byte[] keyIvCombo) { + if (keyIvCombo.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"); + System.arraycopy(keyIvCombo, 0, iv, 0, 16); + System.arraycopy(keyIvCombo, 16, secretKey, 0, 32); + this.aeskey = secretKey; this.iv = iv; - } else if (key.length >= 32) { + } else if (keyIvCombo.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) { + System.arraycopy(keyIvCombo, 0, secretKey, 0, 32); + this.aeskey = secretKey; + } else if (keyIvCombo.length >= 16) { byte[] secretKey = new byte[16]; - System.arraycopy(key, 0, secretKey, 0, 16); - this.aeskey = new SecretKeySpec(secretKey, "AES"); + System.arraycopy(keyIvCombo, 0, secretKey, 0, 16); + this.aeskey = secretKey; } } - public Key getKey() { - return this.aeskey; + public void setKey(byte[] key) { + this.aeskey = key; } - 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); - Logging.d(Config.LOGTAG, "opening encrypted input stream"); - return new CipherInputStream(new FileInputStream(this), cipher); - } catch (NoSuchAlgorithmException e) { - Logging.d(Config.LOGTAG, "no such algo: " + e.getMessage()); - return null; - } catch (NoSuchPaddingException e) { - Logging.d(Config.LOGTAG, "no such padding: " + e.getMessage()); - return null; - } catch (InvalidKeyException e) { - Logging.d(Config.LOGTAG, "invalid key: " + e.getMessage()); - return null; - } catch (InvalidAlgorithmParameterException e) { - Logging.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); - return null; - } catch (FileNotFoundException e) { - return null; - } - } + public void setIv(byte[] iv) { + this.iv = iv; } - 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); - Logging.d(Config.LOGTAG, "opening encrypted output stream"); - return new CipherOutputStream(new FileOutputStream(this), - cipher); - } catch (NoSuchAlgorithmException e) { - Logging.d(Config.LOGTAG, "no such algo: " + e.getMessage()); - return null; - } catch (NoSuchPaddingException e) { - Logging.d(Config.LOGTAG, "no such padding: " + e.getMessage()); - return null; - } catch (InvalidKeyException e) { - Logging.d(Config.LOGTAG, "invalid key: " + e.getMessage()); - return null; - } catch (InvalidAlgorithmParameterException e) { - Logging.d(Config.LOGTAG, "invavid iv:" + e.getMessage()); - return null; - } catch (FileNotFoundException e) { - return null; - } - } + public byte[] getKey() { + return this.aeskey; + } + + public byte[] getIv() { + return this.iv; } } diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java index 9daf90d1..24dc7e94 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java @@ -5,15 +5,17 @@ import java.util.List; import de.thedevstack.conversationsplus.xmpp.jid.Jid; public interface ListItem extends Comparable<ListItem> { - public String getDisplayName(); + String getDisplayName(); - public Jid getJid(); + String getDisplayJid(); - public List<Tag> getTags(); + Jid getJid(); public int getStatusColor(); - public final class Tag { + List<Tag> getTags(); + + final class Tag { private final String name; private final int color; @@ -31,5 +33,5 @@ public interface ListItem extends Comparable<ListItem> { } } - public boolean match(final String needle); + 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 index a5d06f46..d03cab92 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Message.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Message.java @@ -8,9 +8,9 @@ import java.net.URL; import java.util.Arrays; import de.thedevstack.conversationsplus.Config; +import de.thedevstack.conversationsplus.crypto.axolotl.XmppAxolotlSession; import de.thedevstack.conversationsplus.utils.GeoHelper; import de.thedevstack.conversationsplus.utils.MimeUtils; -import de.thedevstack.conversationsplus.utils.UIHelper; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; @@ -34,6 +34,7 @@ public class Message extends AbstractEntity { public static final int ENCRYPTION_OTR = 2; public static final int ENCRYPTION_DECRYPTED = 3; public static final int ENCRYPTION_DECRYPTION_FAILED = 4; + public static final int ENCRYPTION_AXOLOTL = 5; public static final int TYPE_TEXT = 0; public static final int TYPE_IMAGE = 1; @@ -49,9 +50,13 @@ public class Message extends AbstractEntity { public static final String ENCRYPTION = "encryption"; public static final String STATUS = "status"; public static final String TYPE = "type"; + public static final String CARBON = "carbon"; + public static final String EDITED = "edited"; public static final String REMOTE_MSG_ID = "remoteMsgId"; public static final String SERVER_MSG_ID = "serverMsgId"; public static final String RELATIVE_FILE_PATH = "relativeFilePath"; + public static final String FINGERPRINT = "axolotl_fingerprint"; + public static final String READ = "read"; public static final String ME_COMMAND = "/me "; @@ -59,12 +64,14 @@ public class Message extends AbstractEntity { protected String conversationUuid; protected Jid counterpart; protected Jid trueCounterpart; - private String body; + protected String body; protected String encryptedBody; protected long timeSent; protected int encryption; protected int status; protected int type; + protected boolean carbon = false; + protected String edited = null; protected String relativeFilePath; protected boolean read = true; protected String remoteMsgId = null; @@ -73,6 +80,7 @@ public class Message extends AbstractEntity { protected Transferable transferable = null; private Message mNextMessage = null; private Message mPreviousMessage = null; + private String axolotlFingerprint = null; private Message() { @@ -92,16 +100,22 @@ public class Message extends AbstractEntity { encryption, status, TYPE_TEXT, + false, null, null, + null, + null, + true, 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) { + final int encryption, final int status, final int type, final boolean carbon, + final String remoteMsgId, final String relativeFilePath, + final String serverMsgId, final String fingerprint, final boolean read, + final String edited) { this.uuid = uuid; this.conversationUuid = conversationUUid; this.counterpart = counterpart; @@ -111,9 +125,13 @@ public class Message extends AbstractEntity { this.encryption = encryption; this.status = status; this.type = type; + this.carbon = carbon; this.remoteMsgId = remoteMsgId; this.relativeFilePath = relativeFilePath; this.serverMsgId = serverMsgId; + this.axolotlFingerprint = fingerprint; + this.read = read; + this.edited = edited; } public static Message fromCursor(Cursor cursor) { @@ -148,13 +166,17 @@ public class Message extends AbstractEntity { cursor.getInt(cursor.getColumnIndex(ENCRYPTION)), cursor.getInt(cursor.getColumnIndex(STATUS)), cursor.getInt(cursor.getColumnIndex(TYPE)), + cursor.getInt(cursor.getColumnIndex(CARBON)) > 0, cursor.getString(cursor.getColumnIndex(REMOTE_MSG_ID)), cursor.getString(cursor.getColumnIndex(RELATIVE_FILE_PATH)), - cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID))); + cursor.getString(cursor.getColumnIndex(SERVER_MSG_ID)), + cursor.getString(cursor.getColumnIndex(FINGERPRINT)), + cursor.getInt(cursor.getColumnIndex(READ)) > 0, + cursor.getString(cursor.getColumnIndex(EDITED))); } public static Message createStatusMessage(Conversation conversation, String body) { - Message message = new Message(); + final Message message = new Message(); message.setType(Message.TYPE_STATUS); message.setConversation(conversation); message.setBody(body); @@ -181,9 +203,13 @@ public class Message extends AbstractEntity { values.put(ENCRYPTION, encryption); values.put(STATUS, status); values.put(TYPE, type); + values.put(CARBON, carbon ? 1 : 0); values.put(REMOTE_MSG_ID, remoteMsgId); values.put(RELATIVE_FILE_PATH, relativeFilePath); values.put(SERVER_MSG_ID, serverMsgId); + values.put(FINGERPRINT, axolotlFingerprint); + values.put(READ,read ? 1 : 0); + values.put(EDITED, edited); return values; } @@ -304,10 +330,30 @@ public class Message extends AbstractEntity { this.type = type; } + public boolean isCarbon() { + return carbon; + } + + public void setCarbon(boolean carbon) { + this.carbon = carbon; + } + + public void setEdited(String edited) { + this.edited = edited; + } + + public boolean edited() { + return this.edited != null; + } + public void setTrueCounterpart(Jid trueCounterpart) { this.trueCounterpart = trueCounterpart; } + public Jid getTrueCounterpart() { + return this.trueCounterpart; + } + public Transferable getTransferable() { return this.transferable; } @@ -333,7 +379,9 @@ public class Message extends AbstractEntity { if (message.getRemoteMsgId() != null) { return (message.getRemoteMsgId().equals(this.remoteMsgId) || message.getRemoteMsgId().equals(this.uuid)) && this.counterpart.equals(message.getCounterpart()) - && body.equals(otherBody); + && (body.equals(otherBody) + ||(message.getEncryption() == Message.ENCRYPTION_PGP + && message.getRemoteMsgId().matches("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}"))) ; } else { return this.remoteMsgId == null && this.counterpart.equals(message.getCounterpart()) @@ -371,25 +419,43 @@ public class Message extends AbstractEntity { } } + public boolean isLastCorrectableMessage() { + Message next = next(); + while(next != null) { + if (next.isCorrectable()) { + return false; + } + next = next.next(); + } + return isCorrectable(); + } + + private boolean isCorrectable() { + return getStatus() != STATUS_RECEIVED && !isCarbon(); + } + public boolean mergeable(final Message message) { return message != null && (message.getType() == Message.TYPE_TEXT && this.getTransferable() == null && message.getTransferable() == null && message.getEncryption() != Message.ENCRYPTION_PGP && + message.getEncryption() != Message.ENCRYPTION_DECRYPTION_FAILED && this.getType() == message.getType() && //this.getStatus() == message.getStatus() && isStatusMergeable(this.getStatus(), message.getStatus()) && this.getEncryption() == message.getEncryption() && this.getCounterpart() != null && this.getCounterpart().equals(message.getCounterpart()) && + this.edited() == message.edited() && (message.getTimeSent() - this.getTimeSent()) <= (Config.MESSAGE_MERGE_WINDOW * 1000) && !GeoHelper.isGeoUri(message.getBody()) && !GeoHelper.isGeoUri(this.getBody()) && message.treatAsDownloadable() == Decision.NEVER && this.treatAsDownloadable() == Decision.NEVER && !message.getBody().startsWith(ME_COMMAND) && - !this.getBody().startsWith(ME_COMMAND) + !this.getBody().startsWith(ME_COMMAND) && + this.isTrusted() == message.isTrusted() ); } @@ -405,11 +471,14 @@ public class Message extends AbstractEntity { } public String getMergedBody() { - final Message next = this.next(); - if (this.mergeable(next)) { - return getBody() + MERGE_SEPARATOR + next.getMergedBody(); + StringBuilder body = new StringBuilder(this.body); + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + body.append(MERGE_SEPARATOR); + body.append(current.getBody()); } - return getBody(); + return body.toString(); } public boolean hasMeCommand() { @@ -417,20 +486,23 @@ public class Message extends AbstractEntity { } public int getMergedStatus() { - final Message next = this.next(); - if (this.mergeable(next)) { - return next.getStatus(); + int status = this.status; + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + status = current.status; } - return getStatus(); + return status; } public long getMergedTimeSent() { - Message next = this.next(); - if (this.mergeable(next)) { - return next.getMergedTimeSent(); - } else { - return getTimeSent(); + long time = this.timeSent; + Message current = this; + while(current.mergeable(current.next())) { + current = current.next(); + time = current.timeSent; } + return time; } public boolean wasMergedIntoPrevious() { @@ -463,6 +535,14 @@ public class Message extends AbstractEntity { } } + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getEditedId() { + return edited; + } + public enum Decision { MUST, SHOULD, @@ -478,18 +558,15 @@ public class Message extends AbstractEntity { if (path == null || path.isEmpty()) { return null; } - + String filename = path.substring(path.lastIndexOf('/') + 1).toLowerCase(); - int dotPosition = filename.lastIndexOf("."); - if (dotPosition != -1) - { + if (dotPosition != -1) { String extension = filename.substring(dotPosition + 1); - // we want the real file extension, not the crypto one if (Arrays.asList(Transferable.VALID_CRYPTO_EXTENSIONS).contains(extension)) { - return extractRelevantExtension(path.substring(0,dotPosition)); + return extractRelevantExtension(filename.substring(0,dotPosition)); } else { return extension; } @@ -673,4 +750,55 @@ public class Message extends AbstractEntity { public int width = 0; public int height = 0; } + + public void setAxolotlFingerprint(String fingerprint) { + this.axolotlFingerprint = fingerprint; + } + + public String getAxolotlFingerprint() { + return axolotlFingerprint; + } + + public boolean isTrusted() { + XmppAxolotlSession.Trust t = conversation.getAccount().getAxolotlService().getFingerprintTrust(axolotlFingerprint); + return t != null && t.trusted(); + } + + private int getPreviousEncryption() { + for (Message iterator = this.prev(); iterator != null; iterator = iterator.prev()){ + if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + continue; + } + return iterator.getEncryption(); + } + return ENCRYPTION_NONE; + } + + private int getNextEncryption() { + for (Message iterator = this.next(); iterator != null; iterator = iterator.next()){ + if( iterator.isCarbon() || iterator.getStatus() == STATUS_RECEIVED ) { + continue; + } + return iterator.getEncryption(); + } + return conversation.getNextEncryption(); + } + + public boolean isValidInSession() { + int pastEncryption = getCleanedEncryption(this.getPreviousEncryption()); + int futureEncryption = getCleanedEncryption(this.getNextEncryption()); + + boolean inUnencryptedSession = pastEncryption == ENCRYPTION_NONE + || futureEncryption == ENCRYPTION_NONE + || pastEncryption != futureEncryption; + + return inUnencryptedSession || getCleanedEncryption(this.getEncryption()) == pastEncryption; + } + + private static int getCleanedEncryption(int encryption) { + if (encryption == ENCRYPTION_DECRYPTED || encryption == ENCRYPTION_DECRYPTION_FAILED) { + return ENCRYPTION_PGP; + } + return encryption; + } } diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java b/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java index 22155f3e..f0eb83de 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java @@ -1,21 +1,31 @@ package de.thedevstack.conversationsplus.entities; +import android.annotation.SuppressLint; + import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.Map; import de.thedevstack.conversationsplus.R; -import de.thedevstack.conversationsplus.crypto.PgpEngine; -import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.forms.Data; +import de.thedevstack.conversationsplus.xmpp.forms.Field; import de.thedevstack.conversationsplus.xmpp.jid.InvalidJidException; import de.thedevstack.conversationsplus.xmpp.jid.Jid; -import de.thedevstack.conversationsplus.xmpp.stanzas.PresencePacket; - -import android.annotation.SuppressLint; +import de.thedevstack.conversationsplus.xmpp.pep.Avatar; @SuppressLint("DefaultLocale") public class MucOptions { + public Account getAccount() { + return this.conversation.getAccount(); + } + + public void setSelf(User user) { + this.self = user; + } + public enum Affiliation { OWNER("owner", 4, R.string.owner), ADMIN("admin", 3, R.string.admin), @@ -23,7 +33,7 @@ public class MucOptions { OUTCAST("outcast", 0, R.string.outcast), NONE("none", 1, R.string.no_affiliation); - private Affiliation(String string, int rank, int resId) { + Affiliation(String string, int rank, int resId) { this.string = string; this.resId = resId; this.rank = rank; @@ -52,18 +62,20 @@ public class MucOptions { } public enum Role { - MODERATOR("moderator", R.string.moderator), - VISITOR("visitor", R.string.visitor), - PARTICIPANT("participant", R.string.participant), - NONE("none", R.string.no_role); + MODERATOR("moderator", R.string.moderator,3), + VISITOR("visitor", R.string.visitor,1), + PARTICIPANT("participant", R.string.participant,2), + NONE("none", R.string.no_role,0); - private Role(String string, int resId) { + Role(String string, int resId, int rank) { this.string = string; this.resId = resId; + this.rank = rank; } private String string; private int resId; + private int rank; public int getResId() { return resId; @@ -73,51 +85,59 @@ public class MucOptions { 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 boolean ranks(Role role) { + return rank >= role.rank; + } + } - public static final int KICKED_FROM_ROOM = 9; + public enum Error { + NO_RESPONSE, + NONE, + NICK_IN_USE, + PASSWORD_REQUIRED, + BANNED, + MEMBERS_ONLY, + KICKED, + SHUTDOWN, + UNKNOWN + } public static final String STATUS_CODE_ROOM_CONFIG_CHANGED = "104"; public static final String STATUS_CODE_SELF_PRESENCE = "110"; public static final String STATUS_CODE_BANNED = "301"; public static final String STATUS_CODE_CHANGED_NICK = "303"; public static final String STATUS_CODE_KICKED = "307"; - public static final String STATUS_CODE_LOST_MEMBERSHIP = "321"; + public static final String STATUS_CODE_AFFILIATION_CHANGE = "321"; + public static final String STATUS_CODE_LOST_MEMBERSHIP = "322"; + public static final String STATUS_CODE_SHUTDOWN = "332"; private interface OnEventListener { - public void onSuccess(); + void onSuccess(); - public void onFailure(); + void onFailure(); } public interface OnRenameListener extends OnEventListener { } - public interface OnJoinListener extends OnEventListener { - - } - - public class User { + public static class User { private Role role = Role.NONE; private Affiliation affiliation = Affiliation.NONE; - private String name; private Jid jid; + private Jid fullJid; private long pgpKeyId = 0; + private Avatar avatar; + private MucOptions options; - public String getName() { - return name; + public User(MucOptions options, Jid from) { + this.options = options; + this.fullJid = from; } - public void setName(String user) { - this.name = user; + public String getName() { + return this.fullJid.getResourcepart(); } public void setJid(Jid jid) { @@ -158,7 +178,7 @@ public class MucOptions { return false; } else { User o = (User) other; - return name != null && name.equals(o.name) + return getName() != null && getName().equals(o.getName()) && jid != null && jid.equals(o.jid) && affiliation == o.affiliation && role == o.role; @@ -198,26 +218,48 @@ public class MucOptions { } public Contact getContact() { - return account.getRoster().getContactFromRoster(getJid()); + return getAccount().getRoster().getContactFromRoster(getJid()); + } + + public boolean setAvatar(Avatar avatar) { + if (this.avatar != null && this.avatar.equals(avatar)) { + return false; + } else { + this.avatar = avatar; + return true; + } + } + + public String getAvatar() { + return avatar == null ? null : avatar.getFilename(); + } + + public Account getAccount() { + return options.getAccount(); + } + + public Jid getFullJid() { + return fullJid; } } private Account account; - private List<User> users = new CopyOnWriteArrayList<>(); + private final Map<String, User> users = Collections.synchronizedMap(new LinkedHashMap<String, User>()); private List<String> features = new ArrayList<>(); + private Data form = new Data(); 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 Error error = Error.NONE; + public OnRenameListener onRenameListener = null; + private User self; private String subject = null; private String password = null; - private boolean mNickChangingInProgress = false; + public boolean mNickChangingInProgress = false; public MucOptions(Conversation conversation) { this.account = conversation.getAccount(); this.conversation = conversation; + this.self = new User(this,createJoinJid(getProposedNick())); } public void updateFeatures(ArrayList<String> features) { @@ -225,18 +267,39 @@ public class MucOptions { this.features.addAll(features); } + public void updateFormData(Data form) { + this.form = form; + } + public boolean hasFeature(String feature) { return this.features.contains(feature); } public boolean canInvite() { - return !membersOnly() || self.getAffiliation().ranks(Affiliation.ADMIN); + Field field = this.form.getFieldByName("muc#roomconfig_allowinvites"); + return !membersOnly() || self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue())); + } + + public boolean canChangeSubject() { + Field field = this.form.getFieldByName("muc#roomconfig_changesubject"); + return self.getRole().ranks(Role.MODERATOR) || (field != null && "1".equals(field.getValue())); + } + + public boolean participating() { + return !online() + || self.getRole().ranks(Role.PARTICIPANT) + || hasFeature("muc_unmoderated"); } public boolean membersOnly() { return hasFeature("muc_membersonly"); } + public boolean mamSupport() { + // Update with "urn:xmpp:mam:1" once we support it + return hasFeature("urn:xmpp:mam:0"); + } + public boolean nonanonymous() { return hasFeature("muc_nonanonymous"); } @@ -245,135 +308,55 @@ public class MucOptions { 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 boolean moderated() { + return hasFeature("muc_moderated"); + } + + public User deleteUser(String name) { + return this.users.remove(name); } 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); - } - } - } + this.users.put(user.getName(), user); } - private void setError(int error) { - this.isOnline = false; + public User findUser(String name) { + return this.users.get(name); + } + + public boolean isUserInRoom(String name) { + return findUser(name) != null; + } + + public void setError(Error error) { + this.isOnline = isOnline && error == Error.NONE; this.error = error; - 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); - } - } + public void setOnline() { + this.isOnline = true; + } + + public ArrayList<User> getUsers() { + return new ArrayList<>(users.values()); + } + + public List<User> getUsers(int max) { + ArrayList<User> users = new ArrayList<>(); + int i = 1; + for(User user : this.users.values()) { + users.add(user); + if (i >= max) { + break; + } else { + ++i; } } - return codes; + return users; } - public List<User> getUsers() { - return this.users; + public int getUserCount() { + return this.users.size(); } public String getProposedNick() { @@ -400,7 +383,7 @@ public class MucOptions { return this.isOnline; } - public int getError() { + public Error getError() { return this.error; } @@ -408,13 +391,9 @@ public class MucOptions { this.onRenameListener = listener; } - public void setOnJoinListener(OnJoinListener listener) { - this.onJoinListener = listener; - } - public void setOffline() { this.users.clear(); - this.error = 0; + this.error = Error.NO_RESPONSE; this.isOnline = false; } @@ -432,8 +411,8 @@ public class MucOptions { public String createNameFromParticipants() { if (users.size() >= 2) { - List<String> names = new ArrayList<String>(); - for (User user : users) { + List<String> names = new ArrayList<>(); + for (User user : getUsers(5)) { Contact contact = user.getContact(); if (contact != null && !contact.getDisplayName().isEmpty()) { names.add(contact.getDisplayName().split("\\s+")[0]); @@ -456,20 +435,21 @@ public class MucOptions { public long[] getPgpKeyIds() { List<Long> ids = new ArrayList<>(); - for (User user : getUsers()) { + for (User user : this.users.values()) { if (user.getPgpKeyId() != 0) { ids.add(user.getPgpKeyId()); } } - long[] primitivLongArray = new long[ids.size()]; + ids.add(account.getPgpId()); + long[] primitiveLongArray = new long[ids.size()]; for (int i = 0; i < ids.size(); ++i) { - primitivLongArray[i] = ids.get(i); + primitiveLongArray[i] = ids.get(i); } - return primitivLongArray; + return primitiveLongArray; } public boolean pgpKeysInUse() { - for (User user : getUsers()) { + for (User user : this.users.values()) { if (user.getPgpKeyId() != 0) { return true; } @@ -478,7 +458,7 @@ public class MucOptions { } public boolean everybodyHasKeys() { - for (User user : getUsers()) { + for (User user : this.users.values()) { if (user.getPgpKeyId() == 0) { return false; } @@ -494,13 +474,9 @@ public class MucOptions { } } - public Jid getTrueCounterpart(String counterpart) { - for (User user : this.getUsers()) { - if (user.getName().equals(counterpart)) { - return user.getJid(); - } - } - return null; + public Jid getTrueCounterpart(String name) { + User user = findUser(name); + return user == null ? null : user.getJid(); } public String getPassword() { diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Presence.java b/src/main/java/de/thedevstack/conversationsplus/entities/Presence.java new file mode 100644 index 00000000..d4f2871d --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Presence.java @@ -0,0 +1,80 @@ +package de.thedevstack.conversationsplus.entities; + +import java.lang.Comparable; +import java.util.Locale; + +import de.thedevstack.conversationsplus.xml.Element; + +public class Presence implements Comparable { + + public enum Status { + CHAT, ONLINE, AWAY, XA, DND, OFFLINE; + + public String toShowString() { + switch(this) { + case CHAT: return "chat"; + case AWAY: return "away"; + case XA: return "xa"; + case DND: return "dnd"; + } + + return null; + } + } + + protected final Status status; + protected ServiceDiscoveryResult disco; + protected final String ver; + protected final String hash; + + private Presence(Status status, String ver, String hash) { + this.status = status; + this.ver = ver; + this.hash = hash; + } + + public static Presence parse(String show, Element caps) { + final String hash = caps == null ? null : caps.getAttribute("hash"); + final String ver = caps == null ? null : caps.getAttribute("ver"); + if (show == null) { + return new Presence(Status.ONLINE, ver, hash); + } else { + switch (show.toLowerCase(Locale.US)) { + case "away": + return new Presence(Status.AWAY, ver, hash); + case "xa": + return new Presence(Status.XA, ver, hash); + case "dnd": + return new Presence(Status.DND, ver, hash); + case "chat": + return new Presence(Status.CHAT, ver, hash); + default: + return new Presence(Status.ONLINE, ver, hash); + } + } + } + + public int compareTo(Object other) { + return this.status.compareTo(((Presence)other).status); + } + + public Status getStatus() { + return this.status; + } + + public boolean hasCaps() { + return ver != null && hash != null; + } + + public String getVer() { + return this.ver; + } + + public String getHash() { + return this.hash; + } + + public void setServiceDiscoveryResult(ServiceDiscoveryResult disco) { + this.disco = disco; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java b/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java index cb984648..d32e931c 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Presences.java @@ -1,29 +1,18 @@ package de.thedevstack.conversationsplus.entities; +import java.util.Collections; import java.util.Hashtable; -import java.util.Iterator; -import java.util.Map.Entry; - -import de.thedevstack.conversationsplus.xml.Element; public class Presences { + private final Hashtable<String, Presence> presences = new Hashtable<>(); - 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() { + public Hashtable<String, Presence> getPresences() { return this.presences; } - public void updatePresence(String resource, int status) { + public void updatePresence(String resource, Presence presence) { synchronized (this.presences) { - this.presences.put(resource, status); + this.presences.put(resource, presence); } } @@ -39,32 +28,10 @@ public class Presences { } } - public int getMostAvailableStatus() { - int status = OFFLINE; + public Presence getMostAvailablePresence() { 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; + if (presences.size() < 1) { return null; } + return Collections.min(presences.values()); } } diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/ServiceDiscoveryResult.java b/src/main/java/de/thedevstack/conversationsplus/entities/ServiceDiscoveryResult.java new file mode 100644 index 00000000..cfba7c4f --- /dev/null +++ b/src/main/java/de/thedevstack/conversationsplus/entities/ServiceDiscoveryResult.java @@ -0,0 +1,265 @@ +package de.thedevstack.conversationsplus.entities; + +import android.content.ContentValues; +import android.database.Cursor; +import android.util.Base64; +import java.io.UnsupportedEncodingException; +import java.lang.Comparable; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import de.thedevstack.conversationsplus.xml.Element; +import de.thedevstack.conversationsplus.xmpp.forms.Data; +import de.thedevstack.conversationsplus.xmpp.stanzas.IqPacket; + +public class ServiceDiscoveryResult { + public static final String TABLENAME = "discovery_results"; + public static final String HASH = "hash"; + public static final String VER = "ver"; + public static final String RESULT = "result"; + + protected static String blankNull(String s) { + return s == null ? "" : s; + } + + public static class Identity implements Comparable { + protected final String category; + protected final String type; + protected final String lang; + protected final String name; + + public Identity(final String category, final String type, final String lang, final String name) { + this.category = category; + this.type = type; + this.lang = lang; + this.name = name; + } + + public Identity(final Element el) { + this( + el.getAttribute("category"), + el.getAttribute("type"), + el.getAttribute("xml:lang"), + el.getAttribute("name") + ); + } + + public Identity(final JSONObject o) { + this( + o.optString("category", null), + o.optString("type", null), + o.optString("lang", null), + o.optString("name", null) + ); + } + + public String getCategory() { + return this.category; + } + + public String getType() { + return this.type; + } + + public String getLang() { + return this.lang; + } + + public String getName() { + return this.name; + } + + public int compareTo(Object other) { + Identity o = (Identity)other; + int r = blankNull(this.getCategory()).compareTo(blankNull(o.getCategory())); + if(r == 0) { + r = blankNull(this.getType()).compareTo(blankNull(o.getType())); + } + if(r == 0) { + r = blankNull(this.getLang()).compareTo(blankNull(o.getLang())); + } + if(r == 0) { + r = blankNull(this.getName()).compareTo(blankNull(o.getName())); + } + + return r; + } + + public JSONObject toJSON() { + try { + JSONObject o = new JSONObject(); + o.put("category", this.getCategory()); + o.put("type", this.getType()); + o.put("lang", this.getLang()); + o.put("name", this.getName()); + return o; + } catch(JSONException e) { + return null; + } + } + } + + protected final String hash; + protected final byte[] ver; + protected final List<Identity> identities; + protected final List<String> features; + protected final List<Data> forms; + + public ServiceDiscoveryResult(final IqPacket packet) { + this.identities = new ArrayList<>(); + this.features = new ArrayList<>(); + this.forms = new ArrayList<>(); + this.hash = "sha-1"; // We only support sha-1 for now + + final List<Element> elements = packet.query().getChildren(); + + for (final Element element : elements) { + if (element.getName().equals("identity")) { + Identity id = new Identity(element); + if (id.getType() != null && id.getCategory() != null) { + identities.add(id); + } + } else if (element.getName().equals("feature")) { + if (element.getAttribute("var") != null) { + features.add(element.getAttribute("var")); + } + } else if (element.getName().equals("x") && "jabber:x:data".equals(element.getAttribute("xmlns"))) { + forms.add(Data.parse(element)); + } + } + this.ver = this.mkCapHash(); + } + + public ServiceDiscoveryResult(String hash, byte[] ver, JSONObject o) throws JSONException { + this.identities = new ArrayList<>(); + this.features = new ArrayList<>(); + this.forms = new ArrayList<>(); + this.hash = hash; + this.ver = ver; + + JSONArray identities = o.optJSONArray("identities"); + if (identities != null) { + for (int i = 0; i < identities.length(); i++) { + this.identities.add(new Identity(identities.getJSONObject(i))); + } + } + JSONArray features = o.optJSONArray("features"); + if (features != null) { + for (int i = 0; i < features.length(); i++) { + this.features.add(features.getString(i)); + } + } + } + + public String getVer() { + return new String(Base64.encode(this.ver, Base64.DEFAULT)); + } + + public ServiceDiscoveryResult(Cursor cursor) throws JSONException { + this( + cursor.getString(cursor.getColumnIndex(HASH)), + Base64.decode(cursor.getString(cursor.getColumnIndex(VER)), Base64.DEFAULT), + new JSONObject(cursor.getString(cursor.getColumnIndex(RESULT))) + ); + } + + public List<Identity> getIdentities() { + return this.identities; + } + + public List<String> getFeatures() { + return this.features; + } + + public boolean hasIdentity(String category, String type) { + for(Identity id : this.getIdentities()) { + if((category == null || id.getCategory().equals(category)) && + (type == null || id.getType().equals(type))) { + return true; + } + } + + return false; + } + + protected byte[] mkCapHash() { + StringBuilder s = new StringBuilder(); + + List<Identity> identities = this.getIdentities(); + Collections.sort(identities); + + for(Identity id : identities) { + s.append( + blankNull(id.getCategory()) + "/" + + blankNull(id.getType()) + "/" + + blankNull(id.getLang()) + "/" + + blankNull(id.getName()) + "<" + ); + } + + List<String> features = this.getFeatures(); + Collections.sort(features); + + for (String feature : features) { + s.append(feature + "<"); + } + + Collections.sort(forms, new Comparator<Data>() { + @Override + public int compare(Data lhs, Data rhs) { + return lhs.getFormType().compareTo(rhs.getFormType()); + } + }); + + for(Data form : forms) { + s.append(form.getFormType()+"<"); + //TODO append fields and values + } + + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-1"); + } catch (NoSuchAlgorithmException e) { + return null; + } + + try { + return md.digest(s.toString().getBytes("UTF-8")); + } catch(UnsupportedEncodingException e) { + return null; + } + } + + public JSONObject toJSON() { + try { + JSONObject o = new JSONObject(); + + JSONArray ids = new JSONArray(); + for(Identity id : this.getIdentities()) { + ids.put(id.toJSON()); + } + o.put("identites", ids); + + o.put("features", new JSONArray(this.getFeatures())); + + return o; + } catch(JSONException e) { + return null; + } + } + + public ContentValues getContentValues() { + final ContentValues values = new ContentValues(); + values.put(HASH, this.hash); + values.put(VER, getVer()); + values.put(RESULT, this.toJSON().toString()); + return values; + } +} diff --git a/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java b/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java index a5bdb5d7..b03d0fe0 100644 --- a/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java +++ b/src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java @@ -4,7 +4,7 @@ public interface Transferable { String[] VALID_IMAGE_EXTENSIONS = {"webp", "jpeg", "jpg", "png", "jpe"}; String[] VALID_CRYPTO_EXTENSIONS = {"pgp", "gpg", "otr"}; - String[] WELL_KNOWN_EXTENSIONS = {"pdf","m4a"}; + String[] WELL_KNOWN_EXTENSIONS = {"pdf","m4a","mp4"}; int STATUS_UNKNOWN = 0x200; int STATUS_CHECKING = 0x201; |