aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/de/thedevstack/conversationsplus/entities
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/de/thedevstack/conversationsplus/entities')
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/Account.java171
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/Bookmark.java31
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/Contact.java193
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/Conversation.java259
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/DownloadableFile.java129
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/ListItem.java12
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/Message.java182
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/MucOptions.java336
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/Presence.java80
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/Presences.java49
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/ServiceDiscoveryResult.java265
-rw-r--r--src/main/java/de/thedevstack/conversationsplus/entities/Transferable.java2
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;