aboutsummaryrefslogtreecommitdiffstats
path: root/src/eu/siacs/conversations
diff options
context:
space:
mode:
Diffstat (limited to 'src/eu/siacs/conversations')
-rw-r--r--src/eu/siacs/conversations/crypto/OtrEngine.java232
-rw-r--r--src/eu/siacs/conversations/crypto/PgpEngine.java148
-rw-r--r--src/eu/siacs/conversations/entities/AbstractEntity.java25
-rw-r--r--src/eu/siacs/conversations/entities/Account.java221
-rw-r--r--src/eu/siacs/conversations/entities/Contact.java296
-rw-r--r--src/eu/siacs/conversations/entities/Conversation.java284
-rw-r--r--src/eu/siacs/conversations/entities/Message.java144
-rw-r--r--src/eu/siacs/conversations/entities/MucOptions.java5
-rw-r--r--src/eu/siacs/conversations/entities/Presences.java76
-rw-r--r--src/eu/siacs/conversations/persistance/DatabaseBackend.java284
-rw-r--r--src/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java5
-rw-r--r--src/eu/siacs/conversations/services/XmppConnectionService.java946
-rw-r--r--src/eu/siacs/conversations/ui/ConversationActivity.java481
-rw-r--r--src/eu/siacs/conversations/ui/ConversationFragment.java602
-rw-r--r--src/eu/siacs/conversations/ui/DialogContactDetails.java218
-rw-r--r--src/eu/siacs/conversations/ui/EditAccount.java138
-rw-r--r--src/eu/siacs/conversations/ui/ManageAccountActivity.java312
-rw-r--r--src/eu/siacs/conversations/ui/NewConversationActivity.java332
-rw-r--r--src/eu/siacs/conversations/ui/OnAccountListChangedListener.java5
-rw-r--r--src/eu/siacs/conversations/ui/OnConversationListChangedListener.java5
-rw-r--r--src/eu/siacs/conversations/ui/OnRosterFetchedListener.java9
-rw-r--r--src/eu/siacs/conversations/ui/SettingsActivity.java16
-rw-r--r--src/eu/siacs/conversations/ui/SettingsFragment.java15
-rw-r--r--src/eu/siacs/conversations/ui/XmppActivity.java52
-rw-r--r--src/eu/siacs/conversations/utils/DNSHelper.java93
-rw-r--r--src/eu/siacs/conversations/utils/MessageParser.java149
-rw-r--r--src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java9
-rw-r--r--src/eu/siacs/conversations/utils/PhoneHelper.java87
-rw-r--r--src/eu/siacs/conversations/utils/SASL.java24
-rw-r--r--src/eu/siacs/conversations/utils/UIHelper.java210
-rw-r--r--src/eu/siacs/conversations/utils/Validator.java14
-rw-r--r--src/eu/siacs/conversations/xml/Element.java101
-rw-r--r--src/eu/siacs/conversations/xml/Tag.java99
-rw-r--r--src/eu/siacs/conversations/xml/TagWriter.java64
-rw-r--r--src/eu/siacs/conversations/xml/XmlReader.java102
-rw-r--r--src/eu/siacs/conversations/xmpp/IqPacket.java40
-rw-r--r--src/eu/siacs/conversations/xmpp/MessagePacket.java81
-rw-r--r--src/eu/siacs/conversations/xmpp/OnIqPacketReceived.java7
-rw-r--r--src/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java7
-rw-r--r--src/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java7
-rw-r--r--src/eu/siacs/conversations/xmpp/OnStatusChanged.java7
-rw-r--r--src/eu/siacs/conversations/xmpp/PresencePacket.java13
-rw-r--r--src/eu/siacs/conversations/xmpp/XmppConnection.java446
43 files changed, 6411 insertions, 0 deletions
diff --git a/src/eu/siacs/conversations/crypto/OtrEngine.java b/src/eu/siacs/conversations/crypto/OtrEngine.java
new file mode 100644
index 00000000..eca01a73
--- /dev/null
+++ b/src/eu/siacs/conversations/crypto/OtrEngine.java
@@ -0,0 +1,232 @@
+package eu.siacs.conversations.crypto;
+
+import java.math.BigInteger;
+import java.security.KeyFactory;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.DSAPrivateKeySpec;
+import java.security.spec.DSAPublicKeySpec;
+import java.security.spec.InvalidKeySpecException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import android.content.Context;
+import android.util.Log;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.MessagePacket;
+
+import net.java.otr4j.OtrEngineHost;
+import net.java.otr4j.OtrException;
+import net.java.otr4j.OtrPolicy;
+import net.java.otr4j.OtrPolicyImpl;
+import net.java.otr4j.session.InstanceTag;
+import net.java.otr4j.session.SessionID;
+
+public class OtrEngine implements OtrEngineHost {
+
+ private static final String LOGTAG = "xmppService";
+
+ private Account account;
+ private OtrPolicy otrPolicy;
+ private KeyPair keyPair;
+ private Context context;
+
+ public OtrEngine(Context context, Account account) {
+ this.account = account;
+ this.otrPolicy = new OtrPolicyImpl();
+ this.otrPolicy.setAllowV1(false);
+ this.otrPolicy.setAllowV2(true);
+ this.otrPolicy.setAllowV3(true);
+ this.keyPair = loadKey(account.getKeys());
+ }
+
+ private KeyPair loadKey(JSONObject keys) {
+ if (keys == null) {
+ return null;
+ }
+ try {
+ BigInteger x = new BigInteger(keys.getString("otr_x"),16);
+ BigInteger y = new BigInteger(keys.getString("otr_y"),16);
+ BigInteger p = new BigInteger(keys.getString("otr_p"),16);
+ BigInteger q = new BigInteger(keys.getString("otr_q"),16);
+ BigInteger g = new BigInteger(keys.getString("otr_g"),16);
+ KeyFactory keyFactory = KeyFactory.getInstance("DSA");
+ DSAPublicKeySpec pubKeySpec = new DSAPublicKeySpec(y, p, q, g);
+ DSAPrivateKeySpec privateKeySpec = new DSAPrivateKeySpec(x, p, q, g);
+ PublicKey publicKey = keyFactory.generatePublic(pubKeySpec);
+ PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
+ return new KeyPair(publicKey, privateKey);
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (NoSuchAlgorithmException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (InvalidKeySpecException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return null;
+ }
+
+ private void saveKey() {
+ PublicKey publicKey = keyPair.getPublic();
+ PrivateKey privateKey = keyPair.getPrivate();
+ KeyFactory keyFactory;
+ try {
+ keyFactory = KeyFactory.getInstance("DSA");
+ DSAPrivateKeySpec privateKeySpec = keyFactory.getKeySpec(privateKey, DSAPrivateKeySpec.class);
+ DSAPublicKeySpec publicKeySpec = keyFactory.getKeySpec(publicKey, DSAPublicKeySpec.class);
+ this.account.setKey("otr_x",privateKeySpec.getX().toString(16));
+ this.account.setKey("otr_g",privateKeySpec.getG().toString(16));
+ this.account.setKey("otr_p",privateKeySpec.getP().toString(16));
+ this.account.setKey("otr_q",privateKeySpec.getQ().toString(16));
+ this.account.setKey("otr_y",publicKeySpec.getY().toString(16));
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ } catch (InvalidKeySpecException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ @Override
+ public void askForSecret(SessionID arg0, InstanceTag arg1, String arg2) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void finishedSessionMessage(SessionID arg0, String arg1)
+ throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public String getFallbackMessage(SessionID arg0) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public byte[] getLocalFingerprintRaw(SessionID arg0) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ public PublicKey getPublicKey() {
+ return this.keyPair.getPublic();
+ }
+
+ @Override
+ public KeyPair getLocalKeyPair(SessionID arg0) throws OtrException {
+ if (this.keyPair==null) {
+ KeyPairGenerator kg;
+ try {
+ kg = KeyPairGenerator.getInstance("DSA");
+ this.keyPair = kg.genKeyPair();
+ this.saveKey();
+ DatabaseBackend.getInstance(context).updateAccount(account);
+ } catch (NoSuchAlgorithmException e) {
+ Log.d(LOGTAG,"error generating key pair "+e.getMessage());
+ }
+ }
+ return this.keyPair;
+ }
+
+ @Override
+ public String getReplyForUnreadableMessage(SessionID arg0) {
+ // TODO Auto-generated method stub
+ return null;
+ }
+
+ @Override
+ public OtrPolicy getSessionPolicy(SessionID arg0) {
+ return otrPolicy;
+ }
+
+ @Override
+ public void injectMessage(SessionID session, String body) throws OtrException {
+ MessagePacket packet = new MessagePacket();
+ packet.setFrom(account.getFullJid()); //sender
+ packet.setTo(session.getAccountID()+"/"+session.getUserID()); //reciepient
+ packet.setBody(body);
+ Element privateTag = new Element("private");
+ privateTag.setAttribute("xmlns","urn:xmpp:carbons:2");
+ packet.addChild(privateTag);
+ packet.setType(MessagePacket.TYPE_CHAT);
+ account.getXmppConnection().sendMessagePacket(packet);
+ }
+
+ @Override
+ public void messageFromAnotherInstanceReceived(SessionID arg0) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void multipleInstancesDetected(SessionID arg0) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void requireEncryptedMessage(SessionID arg0, String arg1)
+ throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void showError(SessionID arg0, String arg1) throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void smpAborted(SessionID arg0) throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void smpError(SessionID arg0, int arg1, boolean arg2)
+ throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void unencryptedMessageReceived(SessionID arg0, String arg1)
+ throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void unreadableMessageReceived(SessionID arg0) throws OtrException {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void unverify(SessionID arg0, String arg1) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void verify(SessionID arg0, String arg1, boolean arg2) {
+ // TODO Auto-generated method stub
+
+ }
+
+}
diff --git a/src/eu/siacs/conversations/crypto/PgpEngine.java b/src/eu/siacs/conversations/crypto/PgpEngine.java
new file mode 100644
index 00000000..ba000c04
--- /dev/null
+++ b/src/eu/siacs/conversations/crypto/PgpEngine.java
@@ -0,0 +1,148 @@
+package eu.siacs.conversations.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+
+import org.openintents.openpgp.OpenPgpError;
+import org.openintents.openpgp.OpenPgpSignatureResult;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpConstants;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.util.Log;
+
+public class PgpEngine {
+ private OpenPgpApi api;
+
+ public PgpEngine(OpenPgpApi api) {
+ this.api = api;
+ }
+
+ public String decrypt(String message) throws UserInputRequiredException,
+ OpenPgpException {
+ InputStream is = new ByteArrayInputStream(message.getBytes());
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Bundle result = api.decryptAndVerify(is, os);
+ switch (result.getInt(OpenPgpConstants.RESULT_CODE)) {
+ case OpenPgpConstants.RESULT_CODE_SUCCESS:
+ return os.toString();
+ case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ throw new UserInputRequiredException(
+ (PendingIntent) result
+ .getParcelable(OpenPgpConstants.RESULT_INTENT));
+ case OpenPgpConstants.RESULT_CODE_ERROR:
+ throw new OpenPgpException(
+ (OpenPgpError) result
+ .getParcelable(OpenPgpConstants.RESULT_ERRORS));
+ default:
+ return null;
+ }
+ }
+
+ public String encrypt(long keyId, String message) {
+ Bundle params = new Bundle();
+ params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
+ long[] keyIds = { keyId };
+ params.putLongArray(OpenPgpConstants.PARAMS_KEY_IDS, keyIds);
+
+ InputStream is = new ByteArrayInputStream(message.getBytes());
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Bundle result = api.encrypt(params, is, os);
+ StringBuilder encryptedMessageBody = new StringBuilder();
+ String[] lines = os.toString().split("\n");
+ for (int i = 3; i < lines.length - 1; ++i) {
+ encryptedMessageBody.append(lines[i].trim());
+ }
+ return encryptedMessageBody.toString();
+ }
+
+ public long fetchKeyId(String status, String signature)
+ throws OpenPgpException {
+ StringBuilder pgpSig = new StringBuilder();
+ pgpSig.append("-----BEGIN PGP SIGNED MESSAGE-----");
+ pgpSig.append('\n');
+ pgpSig.append("Hash: SHA1");
+ pgpSig.append('\n');
+ pgpSig.append('\n');
+ pgpSig.append(status);
+ pgpSig.append('\n');
+ pgpSig.append("-----BEGIN PGP SIGNATURE-----");
+ pgpSig.append('\n');
+ pgpSig.append('\n');
+ pgpSig.append(signature.replace("\n", "").trim());
+ pgpSig.append('\n');
+ pgpSig.append("-----END PGP SIGNATURE-----");
+ Bundle params = new Bundle();
+ params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
+ InputStream is = new ByteArrayInputStream(pgpSig.toString().getBytes());
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Bundle result = api.decryptAndVerify(params, is, os);
+ switch (result.getInt(OpenPgpConstants.RESULT_CODE)) {
+ case OpenPgpConstants.RESULT_CODE_SUCCESS:
+ OpenPgpSignatureResult sigResult = result
+ .getParcelable(OpenPgpConstants.RESULT_SIGNATURE);
+ return sigResult.getKeyId();
+ case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ break;
+ case OpenPgpConstants.RESULT_CODE_ERROR:
+ throw new OpenPgpException(
+ (OpenPgpError) result
+ .getParcelable(OpenPgpConstants.RESULT_ERRORS));
+ }
+ return 0;
+ }
+
+ public String generateSignature(String status)
+ throws UserInputRequiredException {
+ Bundle params = new Bundle();
+ params.putBoolean(OpenPgpConstants.PARAMS_REQUEST_ASCII_ARMOR, true);
+ InputStream is = new ByteArrayInputStream(status.getBytes());
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+ Bundle result = api.sign(params, is, os);
+ StringBuilder signatureBuilder = new StringBuilder();
+ switch (result.getInt(OpenPgpConstants.RESULT_CODE)) {
+ case OpenPgpConstants.RESULT_CODE_SUCCESS:
+ String[] lines = os.toString().split("\n");
+ for (int i = 7; i < lines.length - 1; ++i) {
+ signatureBuilder.append(lines[i].trim());
+ }
+ break;
+ case OpenPgpConstants.RESULT_CODE_USER_INTERACTION_REQUIRED:
+ UserInputRequiredException exception = new UserInputRequiredException(
+ (PendingIntent) result
+ .getParcelable(OpenPgpConstants.RESULT_INTENT));
+ throw exception;
+ case OpenPgpConstants.RESULT_CODE_ERROR:
+ break;
+ }
+ return signatureBuilder.toString();
+ }
+
+ public class UserInputRequiredException extends Exception {
+ private static final long serialVersionUID = -6913480043269132016L;
+ private PendingIntent pi;
+
+ public UserInputRequiredException(PendingIntent pi) {
+ this.pi = pi;
+ }
+
+ public PendingIntent getPendingIntent() {
+ return this.pi;
+ }
+ }
+
+ public class OpenPgpException extends Exception {
+ private static final long serialVersionUID = -7324789703473056077L;
+ private OpenPgpError error;
+
+ public OpenPgpException(OpenPgpError openPgpError) {
+ this.error = openPgpError;
+ }
+
+ public OpenPgpError getOpenPgpError() {
+ return this.error;
+ }
+ }
+}
diff --git a/src/eu/siacs/conversations/entities/AbstractEntity.java b/src/eu/siacs/conversations/entities/AbstractEntity.java
new file mode 100644
index 00000000..0297fa66
--- /dev/null
+++ b/src/eu/siacs/conversations/entities/AbstractEntity.java
@@ -0,0 +1,25 @@
+package eu.siacs.conversations.entities;
+
+import java.io.Serializable;
+
+import android.content.ContentValues;
+
+public abstract class AbstractEntity implements Serializable {
+
+ private static final long serialVersionUID = -1895605706690653719L;
+
+ public static final String UUID = "uuid";
+
+ protected String uuid;
+
+ public String getUuid() {
+ return this.uuid;
+ }
+
+ public abstract ContentValues getContentValues();
+
+ public boolean equals(AbstractEntity entity) {
+ return this.getUuid().equals(entity.getUuid());
+ }
+
+}
diff --git a/src/eu/siacs/conversations/entities/Account.java b/src/eu/siacs/conversations/entities/Account.java
new file mode 100644
index 00000000..a596aba1
--- /dev/null
+++ b/src/eu/siacs/conversations/entities/Account.java
@@ -0,0 +1,221 @@
+package eu.siacs.conversations.entities;
+
+import java.security.interfaces.DSAPublicKey;
+
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.crypto.OtrEngine;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.util.JsonReader;
+import android.util.Log;
+
+public class Account extends AbstractEntity{
+
+ private static final long serialVersionUID = 6174825093869578035L;
+
+ public static final String TABLENAME = "accounts";
+
+ public static final String USERNAME = "username";
+ public static final String SERVER = "server";
+ public static final String PASSWORD = "password";
+ public static final String OPTIONS = "options";
+ public static final String ROSTERVERSION = "rosterversion";
+ public static final String KEYS = "keys";
+
+ public static final int OPTION_USETLS = 0;
+ public static final int OPTION_DISABLED = 1;
+
+ public static final int STATUS_DISABLED = -1;
+ public static final int STATUS_OFFLINE = 0;
+ public static final int STATUS_ONLINE = 1;
+ public static final int STATUS_UNAUTHORIZED = 2;
+ public static final int STATUS_NOINTERNET = 3;
+ public static final int STATUS_TLS_ERROR = 4;
+ public static final int STATUS_SERVER_NOT_FOUND = 5;
+
+ protected String username;
+ protected String server;
+ protected String password;
+ protected int options = 0;
+ protected String rosterVersion;
+ protected String resource;
+ protected int status = 0;
+ protected JSONObject keys = new JSONObject();
+
+ protected boolean online = false;
+
+ transient OtrEngine otrEngine = null;
+ transient XmppConnection xmppConnection = null;
+
+ private String otrFingerprint;
+
+ public Account() {
+ this.uuid = "0";
+ }
+
+ public Account(String username, String server, String password) {
+ this(java.util.UUID.randomUUID().toString(),username,server,password,0,null,"");
+ }
+ public Account(String uuid, String username, String server,String password, int options, String rosterVersion, String keys) {
+ this.uuid = uuid;
+ this.username = username;
+ this.server = server;
+ this.password = password;
+ this.options = options;
+ this.rosterVersion = rosterVersion;
+ try {
+ this.keys = new JSONObject(keys);
+ } catch (JSONException e) {
+
+ }
+ }
+
+ public boolean isOptionSet(int option) {
+ return ((options & (1 << option)) != 0);
+ }
+
+ public void setOption(int option, boolean value) {
+ if (value) {
+ this.options |= 1 << option;
+ } else {
+ this.options &= ~(1 << option);
+ }
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public String getServer() {
+ return server;
+ }
+
+ public void setServer(String server) {
+ this.server = server;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public int getStatus() {
+ if (isOptionSet(OPTION_DISABLED)) {
+ return STATUS_DISABLED;
+ } else {
+ return this.status;
+ }
+ }
+
+ public void setResource(String resource) {
+ this.resource = resource;
+ }
+
+ public String getJid() {
+ return username+"@"+server;
+ }
+
+ public JSONObject getKeys() {
+ return keys;
+ }
+
+ public boolean setKey(String keyName, String keyValue) {
+ try {
+ this.keys.put(keyName, keyValue);
+ return true;
+ } catch (JSONException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID,uuid);
+ values.put(USERNAME, username);
+ values.put(SERVER, server);
+ values.put(PASSWORD, password);
+ values.put(OPTIONS,options);
+ values.put(KEYS,this.keys.toString());
+ values.put(ROSTERVERSION,rosterVersion);
+ return values;
+ }
+
+ public static Account fromCursor(Cursor cursor) {
+ return new Account(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(USERNAME)),
+ cursor.getString(cursor.getColumnIndex(SERVER)),
+ cursor.getString(cursor.getColumnIndex(PASSWORD)),
+ cursor.getInt(cursor.getColumnIndex(OPTIONS)),
+ cursor.getString(cursor.getColumnIndex(ROSTERVERSION)),
+ cursor.getString(cursor.getColumnIndex(KEYS))
+ );
+ }
+
+
+ public OtrEngine getOtrEngine(Context context) {
+ if (otrEngine==null) {
+ otrEngine = new OtrEngine(context,this);
+ }
+ return this.otrEngine;
+ }
+
+ public XmppConnection getXmppConnection() {
+ return this.xmppConnection;
+ }
+
+ public void setXmppConnection(XmppConnection connection) {
+ this.xmppConnection = connection;
+ }
+
+ public String getFullJid() {
+ return this.getJid()+"/"+this.resource;
+ }
+
+ public String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ DSAPublicKey pubkey = (DSAPublicKey) this.otrEngine.getPublicKey();
+ StringBuilder builder = new StringBuilder(new OtrCryptoEngineImpl().getFingerprint(pubkey));
+ builder.insert(8, " ");
+ builder.insert(17, " ");
+ builder.insert(26, " ");
+ builder.insert(35, " ");
+ this.otrFingerprint = builder.toString();
+ } catch (OtrCryptoException e) {
+
+ }
+ }
+ return this.otrFingerprint;
+ }
+
+ public String getRosterVersion() {
+ if (this.rosterVersion==null) {
+ return "";
+ } else {
+ return this.rosterVersion;
+ }
+ }
+
+ public void setRosterVersion(String version) {
+ this.rosterVersion = version;
+ }
+}
diff --git a/src/eu/siacs/conversations/entities/Contact.java b/src/eu/siacs/conversations/entities/Contact.java
new file mode 100644
index 00000000..0eed39ed
--- /dev/null
+++ b/src/eu/siacs/conversations/entities/Contact.java
@@ -0,0 +1,296 @@
+package eu.siacs.conversations.entities;
+
+import java.io.Serializable;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.Set;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.xml.Element;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class Contact extends AbstractEntity implements Serializable {
+ private static final long serialVersionUID = -4570817093119419962L;
+
+ public static final String TABLENAME = "contacts";
+
+ public static final String DISPLAYNAME = "name";
+ public static final String JID = "jid";
+ public static final String SUBSCRIPTION = "subscription";
+ public static final String SYSTEMACCOUNT = "systemaccount";
+ public static final String PHOTOURI = "photouri";
+ public static final String KEYS = "pgpkey";
+ public static final String PRESENCES = "presences";
+ public static final String ACCOUNT = "accountUuid";
+
+ protected String accountUuid;
+ protected String displayName;
+ protected String jid;
+ protected int subscription = 0;
+ protected String systemAccount;
+ protected String photoUri;
+ protected JSONObject keys = new JSONObject();
+ protected Presences presences = new Presences();
+
+ protected Account account;
+
+ protected boolean inRoster = true;
+
+ public Contact(Account account, String displayName, String jid,
+ String photoUri) {
+ if (account == null) {
+ this.accountUuid = null;
+ } else {
+ this.accountUuid = account.getUuid();
+ }
+ this.account = account;
+ this.displayName = displayName;
+ this.jid = jid;
+ this.photoUri = photoUri;
+ this.uuid = java.util.UUID.randomUUID().toString();
+ }
+
+ public Contact(String uuid, String account, String displayName, String jid,
+ int subscription, String photoUri, String systemAccount,
+ String keys, String presences) {
+ this.uuid = uuid;
+ this.accountUuid = account;
+ this.displayName = displayName;
+ this.jid = jid;
+ this.subscription = subscription;
+ this.photoUri = photoUri;
+ this.systemAccount = systemAccount;
+ if (keys == null) {
+ keys = "";
+ }
+ try {
+ this.keys = new JSONObject(keys);
+ } catch (JSONException e) {
+ this.keys = new JSONObject();
+ }
+ this.presences = Presences.fromJsonString(presences);
+ }
+
+ public String getDisplayName() {
+ return this.displayName;
+ }
+
+ public String getProfilePhoto() {
+ return this.photoUri;
+ }
+
+ public String getJid() {
+ return this.jid;
+ }
+
+ public boolean match(String needle) {
+ return (jid.toLowerCase().contains(needle.toLowerCase()) || (displayName
+ .toLowerCase().contains(needle.toLowerCase())));
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(ACCOUNT, accountUuid);
+ values.put(DISPLAYNAME, displayName);
+ values.put(JID, jid);
+ values.put(SUBSCRIPTION, subscription);
+ values.put(SYSTEMACCOUNT, systemAccount);
+ values.put(PHOTOURI, photoUri);
+ values.put(KEYS, keys.toString());
+ values.put(PRESENCES, presences.toJsonString());
+ return values;
+ }
+
+ public static Contact fromCursor(Cursor cursor) {
+ return new Contact(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(DISPLAYNAME)),
+ cursor.getString(cursor.getColumnIndex(JID)),
+ cursor.getInt(cursor.getColumnIndex(SUBSCRIPTION)),
+ cursor.getString(cursor.getColumnIndex(PHOTOURI)),
+ cursor.getString(cursor.getColumnIndex(SYSTEMACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(KEYS)),
+ cursor.getString(cursor.getColumnIndex(PRESENCES)));
+ }
+
+ public int getSubscription() {
+ return this.subscription;
+ }
+
+ public void setSystemAccount(String account) {
+ this.systemAccount = account;
+ }
+
+ public void setAccount(Account account) {
+ this.account = account;
+ this.accountUuid = account.getUuid();
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public void setUuid(String uuid) {
+ this.uuid = uuid;
+ }
+
+ public boolean couldBeMuc() {
+ String[] split = this.getJid().split("@");
+ if (split.length != 2) {
+ return false;
+ } else {
+ String[] domainParts = split[1].split("\\.");
+ if (domainParts.length < 3) {
+ return false;
+ } else {
+ return (domainParts[0].equals("conf")
+ || domainParts[0].equals("conference") || domainParts[0]
+ .equals("muc"));
+ }
+ }
+ }
+
+ public Hashtable<String, Integer> getPresences() {
+ return this.presences.getPresences();
+ }
+
+ public void updatePresence(String resource, int status) {
+ this.presences.updatePresence(resource, status);
+ }
+
+ public void removePresence(String resource) {
+ this.presences.removePresence(resource);
+ }
+
+ public int getMostAvailableStatus() {
+ return this.presences.getMostAvailableStatus();
+ }
+
+ public void setPresences(Presences pres) {
+ this.presences = pres;
+ }
+
+ public void setPhotoUri(String uri) {
+ this.photoUri = uri;
+ }
+
+ public void setDisplayName(String name) {
+ this.displayName = name;
+ }
+
+ public String getSystemAccount() {
+ return systemAccount;
+ }
+
+ public Set<String> getOtrFingerprints() {
+ Set<String> set = new HashSet<String>();
+ try {
+ if (this.keys.has("otr_fingerprints")) {
+ JSONArray fingerprints = this.keys.getJSONArray("otr_fingerprints");
+ for (int i = 0; i < fingerprints.length(); ++i) {
+ set.add(fingerprints.getString(i));
+ }
+ }
+ } catch (JSONException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ return set;
+ }
+
+ public void addOtrFingerprint(String print) {
+ 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);
+ } catch (JSONException e) {
+
+ }
+ }
+
+ public void setPgpKeyId(long keyId) {
+ try {
+ this.keys.put("pgp_keyid", keyId);
+ } catch (JSONException e) {
+
+ }
+ }
+
+ public long getPgpKeyId() {
+ if (this.keys.has("pgp_keyid")) {
+ try {
+ return this.keys.getLong("pgp_keyid");
+ } catch (JSONException e) {
+ return 0;
+ }
+ } else {
+ return 0;
+ }
+ }
+
+ public void setSubscriptionOption(int option) {
+ this.subscription |= 1 << option;
+ }
+
+ public void resetSubscriptionOption(int option) {
+ this.subscription &= ~(1 << option);
+ }
+
+ public boolean getSubscriptionOption(int option) {
+ return ((this.subscription & (1 << option)) != 0);
+ }
+
+ public void parseSubscriptionFromElement(Element item) {
+ String ask = item.getAttribute("ask");
+ String subscription = item.getAttribute("subscription");
+
+ if (subscription!=null) {
+ if (subscription.equals("to")) {
+ this.resetSubscriptionOption(Contact.Subscription.FROM);
+ this.setSubscriptionOption(Contact.Subscription.TO);
+ } else if (subscription.equals("from")) {
+ this.resetSubscriptionOption(Contact.Subscription.TO);
+ this.setSubscriptionOption(Contact.Subscription.FROM);
+ } else if (subscription.equals("both")) {
+ this.setSubscriptionOption(Contact.Subscription.TO);
+ this.setSubscriptionOption(Contact.Subscription.FROM);
+ }
+ }
+
+ if ((ask!=null)&&(ask.equals("subscribe"))) {
+ this.setSubscriptionOption(Contact.Subscription.ASKING);
+ } else {
+ this.resetSubscriptionOption(Contact.Subscription.ASKING);
+ }
+ }
+
+
+ public class Subscription {
+ public static final int TO = 0;
+ public static final int FROM = 1;
+ public static final int ASKING = 2;
+ public static final int PREEMPTIVE_GRANT = 4;
+ }
+
+
+ public void flagAsNotInRoster() {
+ this.inRoster = false;
+ }
+
+ public boolean isInRoster() {
+ return this.inRoster;
+ }
+}
diff --git a/src/eu/siacs/conversations/entities/Conversation.java b/src/eu/siacs/conversations/entities/Conversation.java
new file mode 100644
index 00000000..d1186a7d
--- /dev/null
+++ b/src/eu/siacs/conversations/entities/Conversation.java
@@ -0,0 +1,284 @@
+package eu.siacs.conversations.entities;
+
+import java.security.interfaces.DSAPublicKey;
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.crypto.OtrEngine;
+import eu.siacs.conversations.xmpp.XmppConnection;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.crypto.OtrCryptoEngineImpl;
+import net.java.otr4j.crypto.OtrCryptoException;
+import net.java.otr4j.session.SessionID;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.util.Log;
+
+public class Conversation extends AbstractEntity {
+
+ private static final long serialVersionUID = -6727528868973996739L;
+
+ public static final String TABLENAME = "conversations";
+
+ public static final int STATUS_AVAILABLE = 0;
+ public static final int STATUS_ARCHIVED = 1;
+ public static final int STATUS_DELETED = 2;
+
+ public static final int MODE_MULTI = 1;
+ public static final int MODE_SINGLE = 0;
+
+ public static final String NAME = "name";
+ public static final String ACCOUNT = "accountUuid";
+ public static final String CONTACT = "contactUuid";
+ public static final String CONTACTJID = "contactJid";
+ public static final String STATUS = "status";
+ public static final String CREATED = "created";
+ public static final String MODE = "mode";
+
+ private String name;
+ private String contactUuid;
+ private String accountUuid;
+ private String contactJid;
+ private int status;
+ private long created;
+ private int mode;
+
+ private transient List<Message> messages = null;
+ private transient Account account = null;
+ private transient Contact contact;
+
+ private transient SessionImpl otrSession;
+
+ private transient String otrFingerprint = null;
+
+ public int nextMessageEncryption = Message.ENCRYPTION_NONE;
+
+ private transient MucOptions mucOptions = null;
+
+ public Conversation(String name, Account account,
+ String contactJid, int mode) {
+ this(java.util.UUID.randomUUID().toString(), name, null, account.getUuid(), contactJid, System
+ .currentTimeMillis(), STATUS_AVAILABLE,mode);
+ this.account = account;
+ }
+
+ public Conversation(String uuid, String name, String contactUuid,
+ String accountUuid, String contactJid, long created, int status, int mode) {
+ this.uuid = uuid;
+ this.name = name;
+ this.contactUuid = contactUuid;
+ this.accountUuid = accountUuid;
+ this.contactJid = contactJid;
+ this.created = created;
+ this.status = status;
+ this.mode = mode;
+ }
+
+ public List<Message> getMessages() {
+ if (messages == null) this.messages = new ArrayList<Message>(); //prevent null pointer
+
+ //populate with Conversation (this)
+
+ for(Message msg : messages) {
+ msg.setConversation(this);
+ }
+
+ return messages;
+ }
+
+ public boolean isRead() {
+ if ((this.messages == null)||(this.messages.size() == 0)) return true;
+ return this.messages.get(this.messages.size() - 1).isRead();
+ }
+
+ public void markRead() {
+ if (this.messages == null) return;
+ for(int i = this.messages.size() -1; i >= 0; --i) {
+ if (messages.get(i).isRead()) return;
+ this.messages.get(i).markRead();
+ }
+ }
+
+ public Message getLatestMessage() {
+ if ((this.messages == null)||(this.messages.size()==0)) {
+ Message message = new Message(this,"",Message.ENCRYPTION_NONE);
+ message.setTime(getCreated());
+ return message;
+ } else {
+ return this.messages.get(this.messages.size() - 1);
+ }
+ }
+
+ public void setMessages(List<Message> msgs) {
+ this.messages = msgs;
+ }
+
+ public String getName() {
+ if (this.contact!=null) {
+ return this.contact.getDisplayName();
+ } else {
+ return this.name;
+ }
+ }
+
+ public String getProfilePhotoString() {
+ if (this.contact==null) {
+ return null;
+ } else {
+ return this.contact.getProfilePhoto();
+ }
+ }
+
+ public String getAccountUuid() {
+ return this.accountUuid;
+ }
+
+ public Account getAccount() {
+ return this.account;
+ }
+
+ public Contact getContact() {
+ return this.contact;
+ }
+
+ public void setContact(Contact contact) {
+ this.contact = contact;
+ if (contact!=null) {
+ this.contactUuid = contact.getUuid();
+ }
+ }
+
+ public void setAccount(Account account) {
+ this.account = account;
+ }
+
+ public String getContactJid() {
+ return this.contactJid;
+ }
+
+ public Uri getProfilePhotoUri() {
+ if (this.getProfilePhotoString() != null) {
+ return Uri.parse(this.getProfilePhotoString());
+ }
+ return null;
+ }
+
+ public int getStatus() {
+ return this.status;
+ }
+
+ public long getCreated() {
+ return this.created;
+ }
+
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(NAME, name);
+ values.put(CONTACT, contactUuid);
+ values.put(ACCOUNT, accountUuid);
+ values.put(CONTACTJID, contactJid);
+ values.put(CREATED, created);
+ values.put(STATUS, status);
+ values.put(MODE,mode);
+ return values;
+ }
+
+ public static Conversation fromCursor(Cursor cursor) {
+ return new Conversation(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(NAME)),
+ cursor.getString(cursor.getColumnIndex(CONTACT)),
+ cursor.getString(cursor.getColumnIndex(ACCOUNT)),
+ cursor.getString(cursor.getColumnIndex(CONTACTJID)),
+ cursor.getLong(cursor.getColumnIndex(CREATED)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)),
+ cursor.getInt(cursor.getColumnIndex(MODE)));
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public int getMode() {
+ return this.mode;
+ }
+
+ public void setMode(int mode) {
+ this.mode = mode;
+ }
+
+ public void startOtrSession(Context context, String presence) {
+ Log.d("xmppService","starting otr session with "+presence);
+ SessionID sessionId = new SessionID(this.getContactJid(),presence,"xmpp");
+ this.otrSession = new SessionImpl(sessionId, getAccount().getOtrEngine(context));
+ try {
+ this.otrSession.startSession();
+ } catch (OtrException e) {
+ Log.d("xmppServic","couldnt start otr");
+ }
+ }
+
+ public SessionImpl getOtrSession() {
+ return this.otrSession;
+ }
+
+ public void resetOtrSession() {
+ this.otrSession = null;
+ }
+
+ public void endOtrIfNeeded() throws OtrException {
+ if (this.otrSession!=null) {
+ if (this.otrSession.getSessionStatus() == SessionStatus.ENCRYPTED) {
+ this.otrSession.endSession();
+ }
+ }
+ this.resetOtrSession();
+ }
+
+ public boolean hasValidOtrSession() {
+ if (this.otrSession == null) {
+ return false;
+ } else {
+ String foreignPresence = this.otrSession.getSessionID().getUserID();
+ if (!getContact().getPresences().containsKey(foreignPresence)) {
+ this.resetOtrSession();
+ return false;
+ }
+ return true;
+ }
+ }
+
+ public String getOtrFingerprint() {
+ if (this.otrFingerprint == null) {
+ try {
+ DSAPublicKey remotePubKey = (DSAPublicKey) getOtrSession().getRemotePublicKey();
+ StringBuilder builder = new StringBuilder(new OtrCryptoEngineImpl().getFingerprint(remotePubKey));
+ builder.insert(8, " ");
+ builder.insert(17, " ");
+ builder.insert(26, " ");
+ builder.insert(35, " ");
+ this.otrFingerprint = builder.toString();
+ } catch (OtrCryptoException e) {
+
+ }
+ }
+ return this.otrFingerprint;
+ }
+
+ public MucOptions getMucOptions() {
+ if (this.mucOptions == null) {
+ this.mucOptions = new MucOptions();
+ }
+ return this.mucOptions ;
+ }
+
+ public void resetMucOptions() {
+ this.mucOptions = null;
+ }
+}
diff --git a/src/eu/siacs/conversations/entities/Message.java b/src/eu/siacs/conversations/entities/Message.java
new file mode 100644
index 00000000..0fce2a5b
--- /dev/null
+++ b/src/eu/siacs/conversations/entities/Message.java
@@ -0,0 +1,144 @@
+package eu.siacs.conversations.entities;
+
+import android.content.ContentValues;
+import android.database.Cursor;
+
+public class Message extends AbstractEntity {
+
+ private static final long serialVersionUID = 7222081895167103025L;
+
+ public static final String TABLENAME = "messages";
+
+ public static final int STATUS_RECIEVED = 0;
+ public static final int STATUS_UNSEND = 1;
+ public static final int STATUS_SEND = 2;
+ public static final int STATUS_ERROR = 3;
+
+ public static final int ENCRYPTION_NONE = 0;
+ public static final int ENCRYPTION_PGP = 1;
+ public static final int ENCRYPTION_OTR = 2;
+ public static final int ENCRYPTION_DECRYPTED = 3;
+
+ public static String CONVERSATION = "conversationUuid";
+ public static String COUNTERPART = "counterpart";
+ public static String BODY = "body";
+ public static String TIME_SENT = "timeSent";
+ public static String ENCRYPTION = "encryption";
+ public static String STATUS = "status";
+
+ protected String conversationUuid;
+ protected String counterpart;
+ protected String body;
+ protected long timeSent;
+ protected int encryption;
+ protected int status;
+ protected boolean read = true;
+
+ protected transient Conversation conversation = null;
+
+ public Message(Conversation conversation, String body, int encryption) {
+ this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),
+ conversation.getContactJid(), body, System.currentTimeMillis(), encryption,
+ Message.STATUS_UNSEND);
+ this.conversation = conversation;
+ }
+
+ public Message(Conversation conversation, String counterpart, String body, int encryption, int status) {
+ this(java.util.UUID.randomUUID().toString(), conversation.getUuid(),counterpart, body, System.currentTimeMillis(), encryption,status);
+ this.conversation = conversation;
+ }
+
+ public Message(String uuid, String conversationUUid, String counterpart,
+ String body, long timeSent, int encryption, int status) {
+ this.uuid = uuid;
+ this.conversationUuid = conversationUUid;
+ this.counterpart = counterpart;
+ this.body = body;
+ this.timeSent = timeSent;
+ this.encryption = encryption;
+ this.status = status;
+ }
+
+ @Override
+ public ContentValues getContentValues() {
+ ContentValues values = new ContentValues();
+ values.put(UUID, uuid);
+ values.put(CONVERSATION, conversationUuid);
+ values.put(COUNTERPART, counterpart);
+ values.put(BODY, body);
+ values.put(TIME_SENT, timeSent);
+ values.put(ENCRYPTION, encryption);
+ values.put(STATUS, status);
+ return values;
+ }
+
+ public String getConversationUuid() {
+ return conversationUuid;
+ }
+
+ public Conversation getConversation() {
+ return this.conversation;
+ }
+
+ public String getCounterpart() {
+ return counterpart;
+ }
+
+ public String getBody() {
+ return body;
+ }
+
+ public long getTimeSent() {
+ return timeSent;
+ }
+
+ public int getEncryption() {
+ return encryption;
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public static Message fromCursor(Cursor cursor) {
+ return new Message(cursor.getString(cursor.getColumnIndex(UUID)),
+ cursor.getString(cursor.getColumnIndex(CONVERSATION)),
+ cursor.getString(cursor.getColumnIndex(COUNTERPART)),
+ cursor.getString(cursor.getColumnIndex(BODY)),
+ cursor.getLong(cursor.getColumnIndex(TIME_SENT)),
+ cursor.getInt(cursor.getColumnIndex(ENCRYPTION)),
+ cursor.getInt(cursor.getColumnIndex(STATUS)));
+ }
+
+ public void setConversation(Conversation conv) {
+ this.conversation = conv;
+ }
+
+ public void setStatus(int status) {
+ this.status = status;
+ }
+
+ public boolean isRead() {
+ return this.read;
+ }
+
+ public void markRead() {
+ this.read = true;
+ }
+
+ public void markUnread() {
+ this.read = false;
+ }
+
+ public void setTime(long time) {
+ this.timeSent = time;
+ }
+
+ public void setEncryption(int encryption) {
+ this.encryption = encryption;
+ }
+
+ public void setBody(String body) {
+ this.body = body;
+ }
+}
diff --git a/src/eu/siacs/conversations/entities/MucOptions.java b/src/eu/siacs/conversations/entities/MucOptions.java
new file mode 100644
index 00000000..4a738e65
--- /dev/null
+++ b/src/eu/siacs/conversations/entities/MucOptions.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.entities;
+
+public class MucOptions {
+
+}
diff --git a/src/eu/siacs/conversations/entities/Presences.java b/src/eu/siacs/conversations/entities/Presences.java
new file mode 100644
index 00000000..af7926a8
--- /dev/null
+++ b/src/eu/siacs/conversations/entities/Presences.java
@@ -0,0 +1,76 @@
+package eu.siacs.conversations.entities;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+public class Presences {
+
+ public static final int CHAT = -1;
+ public static final int ONLINE = 0;
+ public static final int AWAY = 1;
+ public static final int XA = 2;
+ public static final int DND = 3;
+ public static final int OFFLINE = 4;
+
+ private Hashtable<String, Integer> presences = new Hashtable<String, Integer>();
+
+ public Hashtable<String, Integer> getPresences() {
+ return this.presences;
+ }
+
+ public void updatePresence(String resource, int status) {
+ this.presences.put(resource, status);
+ }
+
+ public void removePresence(String resource) {
+ this.presences.remove(resource);
+ }
+
+ public int getMostAvailableStatus() {
+ int status = OFFLINE;
+ 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 String toJsonString() {
+ JSONArray json = new JSONArray();
+ Iterator<Entry<String, Integer>> it = presences.entrySet().iterator();
+
+ while (it.hasNext()) {
+ Entry<String, Integer> entry = it.next();
+ JSONObject jObj = new JSONObject();
+ try {
+ jObj.put("resource", entry.getKey());
+ jObj.put("status", entry.getValue());
+ } catch (JSONException e) {
+
+ }
+ json.put(jObj);
+ }
+ return json.toString();
+ }
+
+ public static Presences fromJsonString(String jsonString) {
+ Presences presences = new Presences();
+ try {
+ JSONArray json = new JSONArray(jsonString);
+ for (int i = 0; i < json.length(); ++i) {
+ JSONObject jObj = json.getJSONObject(i);
+ presences.updatePresence(jObj.getString("resource"),
+ jObj.getInt("status"));
+ }
+ } catch (JSONException e1) {
+
+ }
+ return presences;
+ }
+}
diff --git a/src/eu/siacs/conversations/persistance/DatabaseBackend.java b/src/eu/siacs/conversations/persistance/DatabaseBackend.java
new file mode 100644
index 00000000..1a6f934a
--- /dev/null
+++ b/src/eu/siacs/conversations/persistance/DatabaseBackend.java
@@ -0,0 +1,284 @@
+package eu.siacs.conversations.persistance;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presences;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Bundle;
+import android.util.Log;
+
+public class DatabaseBackend extends SQLiteOpenHelper {
+
+ private static DatabaseBackend instance = null;
+
+ private static final String DATABASE_NAME = "history";
+ private static final int DATABASE_VERSION = 1;
+
+ public DatabaseBackend(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("PRAGMA foreign_keys=ON;");
+ db.execSQL("create table " + Account.TABLENAME + "(" + Account.UUID
+ + " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT,"
+ + Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT,"
+ + Account.ROSTERVERSION + " TEXT," + Account.OPTIONS
+ + " NUMBER, "+Account.KEYS+" TEXT)");
+ db.execSQL("create table " + Conversation.TABLENAME + " ("
+ + Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
+ + " TEXT, " + Conversation.CONTACT + " TEXT, "
+ + Conversation.ACCOUNT + " TEXT, " + Conversation.CONTACTJID
+ + " TEXT, " + Conversation.CREATED + " NUMBER, "
+ + Conversation.STATUS + " NUMBER," + Conversation.MODE
+ + " NUMBER," + "FOREIGN KEY(" + Conversation.ACCOUNT
+ + ") REFERENCES " + Account.TABLENAME + "(" + Account.UUID
+ + ") ON DELETE CASCADE);");
+ db.execSQL("create table " + Message.TABLENAME + "( " + Message.UUID
+ + " TEXT PRIMARY KEY, " + Message.CONVERSATION + " TEXT, "
+ + Message.TIME_SENT + " NUMBER, " + Message.COUNTERPART
+ + " TEXT, " + Message.BODY + " TEXT, " + Message.ENCRYPTION
+ + " NUMBER, " + Message.STATUS + " NUMBER," + "FOREIGN KEY("
+ + Message.CONVERSATION + ") REFERENCES "
+ + Conversation.TABLENAME + "(" + Conversation.UUID
+ + ") ON DELETE CASCADE);");
+ db.execSQL("create table " + Contact.TABLENAME + "(" + Contact.UUID
+ + " TEXT PRIMARY KEY, " + Contact.ACCOUNT + " TEXT, "
+ + Contact.DISPLAYNAME + " TEXT," + Contact.JID + " TEXT,"
+ + Contact.PRESENCES + " TEXT, " + Contact.KEYS
+ + " TEXT," + Contact.PHOTOURI + " TEXT," + Contact.SUBSCRIPTION
+ + " NUMBER," + Contact.SYSTEMACCOUNT + " NUMBER, "
+ + "FOREIGN KEY(" + Contact.ACCOUNT + ") REFERENCES "
+ + Account.TABLENAME + "(" + Account.UUID
+ + ") ON DELETE CASCADE);");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int arg1, int arg2) {
+ // TODO Auto-generated method stub
+
+ }
+
+ public static synchronized DatabaseBackend getInstance(Context context) {
+ if (instance == null) {
+ instance = new DatabaseBackend(context);
+ }
+ return instance;
+ }
+
+ public void createConversation(Conversation conversation) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Conversation.TABLENAME, null, conversation.getContentValues());
+ }
+
+ public void createMessage(Message message) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Message.TABLENAME, null, message.getContentValues());
+ }
+
+ public void createAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Account.TABLENAME, null, account.getContentValues());
+ }
+
+ public void createContact(Contact contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ db.insert(Contact.TABLENAME, null, contact.getContentValues());
+ }
+
+ public int getConversationCount() {
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.rawQuery("select count(uuid) as count from "
+ + Conversation.TABLENAME + " where " + Conversation.STATUS
+ + "=" + Conversation.STATUS_AVAILABLE, null);
+ cursor.moveToFirst();
+ return cursor.getInt(0);
+ }
+
+ public List<Conversation> getConversations(int status) {
+ List<Conversation> list = new ArrayList<Conversation>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { "" + status };
+ Cursor cursor = db.rawQuery("select * from " + Conversation.TABLENAME
+ + " where " + Conversation.STATUS + " = ? order by "
+ + Conversation.CREATED + " desc", selectionArgs);
+ while (cursor.moveToNext()) {
+ list.add(Conversation.fromCursor(cursor));
+ }
+ return list;
+ }
+
+ public List<Message> getMessages(Conversation conversation, int limit) {
+ List<Message> list = new ArrayList<Message>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { conversation.getUuid() };
+ Cursor cursor = db.query(Message.TABLENAME, null, Message.CONVERSATION
+ + "=?", selectionArgs, null, null, Message.TIME_SENT + " DESC",
+ String.valueOf(limit));
+ if (cursor.getCount() > 0) {
+ cursor.moveToLast();
+ do {
+ list.add(Message.fromCursor(cursor));
+ } while (cursor.moveToPrevious());
+ }
+ return list;
+ }
+
+ public Conversation findConversation(Account account, String contactJid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { account.getUuid(), contactJid };
+ Cursor cursor = db.query(Conversation.TABLENAME, null,
+ Conversation.ACCOUNT + "=? AND " + Conversation.CONTACTJID + "=?",
+ selectionArgs, null, null, null);
+ if (cursor.getCount() == 0)
+ return null;
+ cursor.moveToFirst();
+ return Conversation.fromCursor(cursor);
+ }
+
+ public void updateConversation(Conversation conversation) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { conversation.getUuid() };
+ db.update(Conversation.TABLENAME, conversation.getContentValues(),
+ Conversation.UUID + "=?", args);
+ }
+
+ public List<Account> getAccounts() {
+ List<Account> list = new ArrayList<Account>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.query(Account.TABLENAME, null, null, null, null,
+ null, null);
+ Log.d("gultsch", "found " + cursor.getCount() + " accounts");
+ while (cursor.moveToNext()) {
+ list.add(Account.fromCursor(cursor));
+ }
+ return list;
+ }
+
+ public void updateAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { account.getUuid() };
+ db.update(Account.TABLENAME, account.getContentValues(), Account.UUID
+ + "=?", args);
+ }
+
+ public void deleteAccount(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { account.getUuid() };
+ db.delete(Account.TABLENAME, Account.UUID + "=?", args);
+ }
+
+ @Override
+ public SQLiteDatabase getWritableDatabase() {
+ SQLiteDatabase db = super.getWritableDatabase();
+ db.execSQL("PRAGMA foreign_keys=ON;");
+ return db;
+ }
+
+ public void updateMessage(Message message) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { message.getUuid() };
+ db.update(Message.TABLENAME, message.getContentValues(), Message.UUID
+ + "=?", args);
+ }
+
+ public void updateContact(Contact contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { contact.getUuid() };
+ db.update(Contact.TABLENAME, contact.getContentValues(), Contact.UUID
+ + "=?", args);
+ }
+
+ public void clearPresences(Account account) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { account.getUuid() };
+ ContentValues values = new ContentValues();
+ values.put(Contact.PRESENCES,"[]");
+ db.update(Contact.TABLENAME, values, Contact.ACCOUNT
+ + "=?", args);
+ }
+
+ public void mergeContacts(List<Contact> contacts) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ for (int i = 0; i < contacts.size(); i++) {
+ Contact contact = contacts.get(i);
+ String[] columns = {Contact.UUID, Contact.PRESENCES};
+ String[] args = {contact.getAccount().getUuid(), contact.getJid()};
+ Cursor cursor = db.query(Contact.TABLENAME, columns,Contact.ACCOUNT+"=? AND "+Contact.JID+"=?", args, null, null, null);
+ if (cursor.getCount()>=1) {
+ cursor.moveToFirst();
+ contact.setUuid(cursor.getString(0));
+ contact.setPresences(Presences.fromJsonString(cursor.getString(1)));
+ updateContact(contact);
+ } else {
+ contact.setUuid(UUID.randomUUID().toString());
+ createContact(contact);
+ }
+ }
+ }
+
+ public List<Contact> getContacts(Account account) {
+ List<Contact> list = new ArrayList<Contact>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor;
+ if (account==null) {
+ cursor = db.query(Contact.TABLENAME, null, null, null, null,
+ null, null);
+ } else {
+ String args[] = {account.getUuid()};
+ cursor = db.query(Contact.TABLENAME, null, Contact.ACCOUNT+"=?", args, null,
+ null, null);
+ }
+ while (cursor.moveToNext()) {
+ list.add(Contact.fromCursor(cursor));
+ }
+ return list;
+ }
+
+ public List<Contact> getContats(String where) {
+ List<Contact> list = new ArrayList<Contact>();
+ SQLiteDatabase db = this.getReadableDatabase();
+ Cursor cursor = db.query(Contact.TABLENAME, null, where, null, null, null, null);
+ while (cursor.moveToNext()) {
+ list.add(Contact.fromCursor(cursor));
+ }
+ return list;
+ }
+
+ public Contact findContact(Account account, String jid) {
+ SQLiteDatabase db = this.getReadableDatabase();
+ String[] selectionArgs = { account.getUuid(), jid };
+ Cursor cursor = db.query(Contact.TABLENAME, null,
+ Contact.ACCOUNT + "=? AND " + Contact.JID + "=?",
+ selectionArgs, null, null, null);
+ if (cursor.getCount() == 0)
+ return null;
+ cursor.moveToFirst();
+ return Contact.fromCursor(cursor);
+ }
+
+ public void deleteMessage(Message message) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { message.getUuid() };
+ db.delete(Message.TABLENAME, Message.UUID + "=?", args);
+ }
+
+ public void deleteContact(Contact contact) {
+ SQLiteDatabase db = this.getWritableDatabase();
+ String[] args = { contact.getUuid() };
+ db.delete(Contact.TABLENAME, Contact.UUID + "=?", args);
+ }
+
+
+}
diff --git a/src/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java b/src/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java
new file mode 100644
index 00000000..6a457b17
--- /dev/null
+++ b/src/eu/siacs/conversations/persistance/OnPhoneContactsMerged.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.persistance;
+
+public interface OnPhoneContactsMerged {
+ public void phoneContactsMerged();
+}
diff --git a/src/eu/siacs/conversations/services/XmppConnectionService.java b/src/eu/siacs/conversations/services/XmppConnectionService.java
new file mode 100644
index 00000000..218d5088
--- /dev/null
+++ b/src/eu/siacs/conversations/services/XmppConnectionService.java
@@ -0,0 +1,946 @@
+package eu.siacs.conversations.services;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Hashtable;
+import java.util.List;
+
+import org.json.JSONException;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpServiceConnection;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.crypto.PgpEngine.OpenPgpException;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.persistance.DatabaseBackend;
+import eu.siacs.conversations.persistance.OnPhoneContactsMerged;
+import eu.siacs.conversations.ui.OnAccountListChangedListener;
+import eu.siacs.conversations.ui.OnConversationListChangedListener;
+import eu.siacs.conversations.ui.OnRosterFetchedListener;
+import eu.siacs.conversations.utils.MessageParser;
+import eu.siacs.conversations.utils.OnPhoneContactsLoadedListener;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.IqPacket;
+import eu.siacs.conversations.xmpp.MessagePacket;
+import eu.siacs.conversations.xmpp.OnIqPacketReceived;
+import eu.siacs.conversations.xmpp.OnMessagePacketReceived;
+import eu.siacs.conversations.xmpp.OnPresencePacketReceived;
+import eu.siacs.conversations.xmpp.OnStatusChanged;
+import eu.siacs.conversations.xmpp.PresencePacket;
+import eu.siacs.conversations.xmpp.XmppConnection;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.ContentObserver;
+import android.database.DatabaseUtils;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract;
+import android.util.Log;
+
+public class XmppConnectionService extends Service {
+
+ protected static final String LOGTAG = "xmppService";
+ public DatabaseBackend databaseBackend;
+
+ public long startDate;
+
+ private List<Account> accounts;
+ private List<Conversation> conversations = null;
+
+ public OnConversationListChangedListener convChangedListener = null;
+ private OnAccountListChangedListener accountChangedListener = null;
+
+ private ContentObserver contactObserver = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ Log.d(LOGTAG, "contact list has changed");
+ mergePhoneContactsWithRoster(null);
+ }
+ };
+
+ private XmppConnectionService service = this;
+
+ private final IBinder mBinder = new XmppConnectionBinder();
+ private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
+
+ @Override
+ public void onMessagePacketReceived(Account account,
+ MessagePacket packet) {
+ Message message = null;
+ boolean notify = false;
+ if ((packet.getType() == MessagePacket.TYPE_CHAT)) {
+ String pgpBody = MessageParser.getPgpBody(packet);
+ if (pgpBody != null) {
+ message = MessageParser.parsePgpChat(pgpBody, packet,
+ account, service);
+ notify = false;
+ } else if (packet.hasChild("body")
+ && (packet.getBody().startsWith("?OTR"))) {
+ message = MessageParser.parseOtrChat(packet, account,
+ service);
+ notify = true;
+ } else if (packet.hasChild("body")) {
+ message = MessageParser.parsePlainTextChat(packet, account,
+ service);
+ notify = true;
+ } else if (packet.hasChild("received")
+ || (packet.hasChild("sent"))) {
+ message = MessageParser.parseCarbonMessage(packet, account,
+ service);
+ }
+
+ } else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
+ message = MessageParser
+ .parseGroupchat(packet, account, service);
+ if (message != null) {
+ notify = (message.getStatus() == Message.STATUS_RECIEVED);
+ }
+ } else if (packet.getType() == MessagePacket.TYPE_ERROR) {
+ message = MessageParser.parseError(packet, account, service);
+ } else {
+ Log.d(LOGTAG, "unparsed message " + packet.toString());
+ }
+ if (message == null) {
+ return;
+ }
+ if (packet.hasChild("delay")) {
+ try {
+ String stamp = packet.findChild("delay").getAttribute(
+ "stamp");
+ stamp = stamp.replace("Z", "+0000");
+ Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
+ .parse(stamp);
+ message.setTime(date.getTime());
+ } catch (ParseException e) {
+ Log.d(LOGTAG, "error trying to parse date" + e.getMessage());
+ }
+ }
+ if (notify) {
+ message.markUnread();
+ }
+ Conversation conversation = message.getConversation();
+ conversation.getMessages().add(message);
+ if (packet.getType() != MessagePacket.TYPE_ERROR) {
+ databaseBackend.createMessage(message);
+ }
+ if (convChangedListener != null) {
+ convChangedListener.onConversationListChanged();
+ } else {
+ if (notify) {
+ NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ mNotificationManager.notify(2342, UIHelper
+ .getUnreadMessageNotification(
+ getApplicationContext(), conversation));
+ }
+ }
+ }
+ };
+ private OnStatusChanged statusListener = new OnStatusChanged() {
+
+ @Override
+ public void onStatusChanged(Account account) {
+ if (accountChangedListener != null) {
+ accountChangedListener.onAccountListChangedListener();
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ databaseBackend.clearPresences(account);
+ connectMultiModeConversations(account);
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); ++i) {
+ if (conversations.get(i).getAccount() == account) {
+ sendUnsendMessages(conversations.get(i));
+ }
+ }
+ if (convChangedListener != null) {
+ convChangedListener.onConversationListChanged();
+ }
+ if (account.getKeys().has("pgp_signature")) {
+ try {
+ sendPgpPresence(account, account.getKeys().getString("pgp_signature"));
+ } catch (JSONException e) {
+ //
+ }
+ }
+ }
+ }
+ };
+
+ private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
+
+ @Override
+ public void onPresencePacketReceived(Account account,
+ PresencePacket packet) {
+ if (packet.hasChild("x")&&(packet.findChild("x").getAttribute("xmlns").startsWith("http://jabber.org/protocol/muc"))) {
+ Log.d(LOGTAG,"got muc presence "+packet.toString());
+ } else {
+ String[] fromParts = packet.getAttribute("from").split("/");
+ Contact contact = findContact(account, fromParts[0]);
+ if (contact == null) {
+ // most likely self or roster not synced
+ return;
+ }
+ String type = packet.getAttribute("type");
+ if (type == null) {
+ Element show = packet.findChild("show");
+ if (show == null) {
+ contact.updatePresence(fromParts[1], Presences.ONLINE);
+ } else if (show.getContent().equals("away")) {
+ contact.updatePresence(fromParts[1], Presences.AWAY);
+ } else if (show.getContent().equals("xa")) {
+ contact.updatePresence(fromParts[1], Presences.XA);
+ } else if (show.getContent().equals("chat")) {
+ contact.updatePresence(fromParts[1], Presences.CHAT);
+ } else if (show.getContent().equals("dnd")) {
+ contact.updatePresence(fromParts[1], Presences.DND);
+ }
+ PgpEngine pgp = getPgpEngine();
+ if (pgp!=null) {
+ Element x = packet.findChild("x");
+ if ((x != null)
+ && (x.getAttribute("xmlns").equals("jabber:x:signed"))) {
+ try {
+ Log.d(LOGTAG,"pgp signature for contact" +packet.getAttribute("from"));
+ contact.setPgpKeyId(pgp.fetchKeyId(packet.findChild("status")
+ .getContent(), x.getContent()));
+ } catch (OpenPgpException e) {
+ Log.d(LOGTAG,"faulty pgp. just ignore");
+ }
+ }
+ }
+ databaseBackend.updateContact(contact);
+ } else if (type.equals("unavailable")) {
+ if (fromParts.length != 2) {
+ // Log.d(LOGTAG,"received presence with no resource "+packet.toString());
+ } else {
+ contact.removePresence(fromParts[1]);
+ databaseBackend.updateContact(contact);
+ }
+ } else if (type.equals("subscribe")) {
+ if (contact
+ .getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) {
+ sendPresenceUpdatesTo(contact);
+ contact.setSubscriptionOption(Contact.Subscription.FROM);
+ contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
+ replaceContactInConversation(contact.getJid(), contact);
+ databaseBackend.updateContact(contact);
+ if ((contact
+ .getSubscriptionOption(Contact.Subscription.ASKING))
+ && (!contact
+ .getSubscriptionOption(Contact.Subscription.TO))) {
+ requestPresenceUpdatesFrom(contact);
+ }
+ } else {
+ // TODO: ask user to handle it maybe
+ }
+ } else {
+ //Log.d(LOGTAG, packet.toString());
+ }
+ replaceContactInConversation(contact.getJid(), contact);
+ }
+ }
+ };
+
+ private OnIqPacketReceived unknownIqListener = new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.hasChild("query")) {
+ Element query = packet.findChild("query");
+ String xmlns = query.getAttribute("xmlns");
+ if ((xmlns != null) && (xmlns.equals("jabber:iq:roster"))) {
+ processRosterItems(account, query);
+ mergePhoneContactsWithRoster(null);
+ }
+ }
+ }
+ };
+
+ private OpenPgpServiceConnection pgpServiceConnection;
+ private PgpEngine mPgpEngine = null;
+
+ public PgpEngine getPgpEngine() {
+ if (pgpServiceConnection.isBound()) {
+ if (this.mPgpEngine == null) {
+ this.mPgpEngine = new PgpEngine(new OpenPgpApi(
+ getApplicationContext(),
+ pgpServiceConnection.getService()));
+ }
+ return mPgpEngine;
+ } else {
+ return null;
+ }
+
+ }
+
+ private void processRosterItems(Account account, Element elements) {
+ String version = elements.getAttribute("ver");
+ if (version != null) {
+ account.setRosterVersion(version);
+ databaseBackend.updateAccount(account);
+ }
+ for (Element item : elements.getChildren()) {
+ if (item.getName().equals("item")) {
+ String jid = item.getAttribute("jid");
+ String subscription = item.getAttribute("subscription");
+ Contact contact = databaseBackend.findContact(account, jid);
+ if (contact == null) {
+ if (!subscription.equals("remove")) {
+ String name = item.getAttribute("name");
+ if (name == null) {
+ name = jid.split("@")[0];
+ }
+ contact = new Contact(account, name, jid, null);
+ contact.parseSubscriptionFromElement(item);
+ databaseBackend.createContact(contact);
+ }
+ } else {
+ if (subscription.equals("remove")) {
+ databaseBackend.deleteContact(contact);
+ replaceContactInConversation(contact.getJid(), null);
+ } else {
+ contact.parseSubscriptionFromElement(item);
+ databaseBackend.updateContact(contact);
+ replaceContactInConversation(contact.getJid(), contact);
+ }
+ }
+ }
+ }
+ }
+
+ private void replaceContactInConversation(String jid, Contact contact) {
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); ++i) {
+ if ((conversations.get(i).getContactJid().equals(jid))) {
+ conversations.get(i).setContact(contact);
+ break;
+ }
+ }
+ }
+
+ public class XmppConnectionBinder extends Binder {
+ public XmppConnectionService getService() {
+ return XmppConnectionService.this;
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ for (Account account : accounts) {
+ if (account.getXmppConnection() == null) {
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ account.setXmppConnection(this.createConnection(account));
+ }
+ }
+ }
+ return START_STICKY;
+ }
+
+ @Override
+ public void onCreate() {
+ databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
+ this.accounts = databaseBackend.getAccounts();
+
+ getContentResolver().registerContentObserver(
+ ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
+ this.pgpServiceConnection = new OpenPgpServiceConnection(
+ getApplicationContext(), "org.sufficientlysecure.keychain");
+ this.pgpServiceConnection.bindToService();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ for (Account account : accounts) {
+ if (account.getXmppConnection() != null) {
+ disconnect(account);
+ }
+ }
+ }
+
+ public XmppConnection createConnection(Account account) {
+ PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ XmppConnection connection = new XmppConnection(account, pm);
+ connection.setOnMessagePacketReceivedListener(this.messageListener);
+ connection.setOnStatusChangedListener(this.statusListener);
+ connection.setOnPresencePacketReceivedListener(this.presenceListener);
+ connection
+ .setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
+ Thread thread = new Thread(connection);
+ thread.start();
+ return connection;
+ }
+
+ public void sendMessage(Message message, String presence) {
+ Account account = message.getConversation().getAccount();
+ Conversation conv = message.getConversation();
+ boolean saveInDb = false;
+ boolean addToConversation = false;
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ MessagePacket packet;
+ if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+ if (!conv.hasValidOtrSession()) {
+ // starting otr session. messages will be send later
+ conv.startOtrSession(getApplicationContext(), presence);
+ } else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+ // otr session aleary exists, creating message packet
+ // accordingly
+ packet = prepareMessagePacket(account, message,
+ conv.getOtrSession());
+ account.getXmppConnection().sendMessagePacket(packet);
+ message.setStatus(Message.STATUS_SEND);
+ }
+ saveInDb = true;
+ addToConversation = true;
+ } else if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ long keyId = message.getConversation().getContact()
+ .getPgpKeyId();
+ packet = new MessagePacket();
+ packet.setType(MessagePacket.TYPE_CHAT);
+ packet.setFrom(message.getConversation().getAccount()
+ .getFullJid());
+ packet.setTo(message.getCounterpart());
+ packet.setBody("This is an XEP-0027 encryted message");
+ Element x = new Element("x");
+ x.setAttribute("xmlns", "jabber:x:encrypted");
+ x.setContent(this.getPgpEngine().encrypt(keyId,
+ message.getBody()));
+ packet.addChild(x);
+ account.getXmppConnection().sendMessagePacket(packet);
+ message.setStatus(Message.STATUS_SEND);
+ message.setEncryption(Message.ENCRYPTION_DECRYPTED);
+ saveInDb = true;
+ addToConversation = true;
+ } else {
+ // don't encrypt
+ if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
+ message.setStatus(Message.STATUS_SEND);
+ saveInDb = true;
+ addToConversation = true;
+ }
+
+ packet = prepareMessagePacket(account, message, null);
+ account.getXmppConnection().sendMessagePacket(packet);
+ }
+ } else {
+ // account is offline
+ saveInDb = true;
+ addToConversation = true;
+
+ }
+ if (saveInDb) {
+ databaseBackend.createMessage(message);
+ }
+ if (addToConversation) {
+ conv.getMessages().add(message);
+ if (convChangedListener != null) {
+ convChangedListener.onConversationListChanged();
+ }
+ }
+
+ }
+
+ private void sendUnsendMessages(Conversation conversation) {
+ for (int i = 0; i < conversation.getMessages().size(); ++i) {
+ if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) {
+ Message message = conversation.getMessages().get(i);
+ MessagePacket packet = prepareMessagePacket(
+ conversation.getAccount(), message, null);
+ conversation.getAccount().getXmppConnection()
+ .sendMessagePacket(packet);
+ message.setStatus(Message.STATUS_SEND);
+ if (conversation.getMode() == Conversation.MODE_SINGLE) {
+ databaseBackend.updateMessage(message);
+ } else {
+ databaseBackend.deleteMessage(message);
+ conversation.getMessages().remove(i);
+ i--;
+ }
+ }
+ }
+ }
+
+ public MessagePacket prepareMessagePacket(Account account, Message message,
+ Session otrSession) {
+ MessagePacket packet = new MessagePacket();
+ if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
+ packet.setType(MessagePacket.TYPE_CHAT);
+ if (otrSession != null) {
+ try {
+ packet.setBody(otrSession.transformSending(message
+ .getBody()));
+ } catch (OtrException e) {
+ Log.d(LOGTAG,
+ account.getJid()
+ + ": could not encrypt message to "
+ + message.getCounterpart());
+ }
+ Element privateMarker = new Element("private");
+ privateMarker.setAttribute("xmlns", "urn:xmpp:carbons:2");
+ packet.addChild(privateMarker);
+ packet.setTo(otrSession.getSessionID().getAccountID() + "/"
+ + otrSession.getSessionID().getUserID());
+ packet.setFrom(account.getFullJid());
+ } else {
+ packet.setBody(message.getBody());
+ packet.setTo(message.getCounterpart());
+ packet.setFrom(account.getJid());
+ }
+ } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
+ packet.setType(MessagePacket.TYPE_GROUPCHAT);
+ packet.setBody(message.getBody());
+ packet.setTo(message.getCounterpart());
+ packet.setFrom(account.getJid());
+ }
+ return packet;
+ }
+
+ public void getRoster(Account account,
+ final OnRosterFetchedListener listener) {
+ List<Contact> contacts = databaseBackend.getContacts(account);
+ for (int i = 0; i < contacts.size(); ++i) {
+ contacts.get(i).setAccount(account);
+ }
+ if (listener != null) {
+ listener.onRosterFetched(contacts);
+ }
+ }
+
+ public void updateRoster(final Account account,
+ final OnRosterFetchedListener listener) {
+ IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
+ Element query = new Element("query");
+ query.setAttribute("xmlns", "jabber:iq:roster");
+ query.setAttribute("ver", account.getRosterVersion());
+ iqPacket.addChild(query);
+ account.getXmppConnection().sendIqPacket(iqPacket,
+ new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(final Account account,
+ IqPacket packet) {
+ Element roster = packet.findChild("query");
+ if (roster != null) {
+ processRosterItems(account, roster);
+ StringBuilder mWhere = new StringBuilder();
+ mWhere.append("jid NOT IN(");
+ List<Element> items = roster.getChildren();
+ for (int i = 0; i < items.size(); ++i) {
+ mWhere.append(DatabaseUtils
+ .sqlEscapeString(items.get(i)
+ .getAttribute("jid")));
+ if (i != items.size() - 1) {
+ mWhere.append(",");
+ }
+ }
+ mWhere.append(") and accountUuid = \"");
+ mWhere.append(account.getUuid());
+ mWhere.append("\"");
+ List<Contact> contactsToDelete = databaseBackend
+ .getContats(mWhere.toString());
+ for (Contact contact : contactsToDelete) {
+ databaseBackend.deleteContact(contact);
+ replaceContactInConversation(contact.getJid(),
+ null);
+ }
+
+ }
+ mergePhoneContactsWithRoster(new OnPhoneContactsMerged() {
+
+ @Override
+ public void phoneContactsMerged() {
+ if (listener != null) {
+ getRoster(account, listener);
+ }
+ }
+ });
+ }
+ });
+ }
+
+ public void mergePhoneContactsWithRoster(
+ final OnPhoneContactsMerged listener) {
+ PhoneHelper.loadPhoneContacts(getApplicationContext(),
+ new OnPhoneContactsLoadedListener() {
+ @Override
+ public void onPhoneContactsLoaded(
+ Hashtable<String, Bundle> phoneContacts) {
+ List<Contact> contacts = databaseBackend
+ .getContacts(null);
+ for (int i = 0; i < contacts.size(); ++i) {
+ Contact contact = contacts.get(i);
+ if (phoneContacts.containsKey(contact.getJid())) {
+ Bundle phoneContact = phoneContacts.get(contact
+ .getJid());
+ String systemAccount = phoneContact
+ .getInt("phoneid")
+ + "#"
+ + phoneContact.getString("lookup");
+ contact.setSystemAccount(systemAccount);
+ contact.setPhotoUri(phoneContact
+ .getString("photouri"));
+ contact.setDisplayName(phoneContact
+ .getString("displayname"));
+ databaseBackend.updateContact(contact);
+ replaceContactInConversation(contact.getJid(),
+ contact);
+ } else {
+ if ((contact.getSystemAccount() != null)
+ || (contact.getProfilePhoto() != null)) {
+ contact.setSystemAccount(null);
+ contact.setPhotoUri(null);
+ databaseBackend.updateContact(contact);
+ replaceContactInConversation(
+ contact.getJid(), contact);
+ }
+ }
+ }
+ if (listener != null) {
+ listener.phoneContactsMerged();
+ }
+ }
+ });
+ }
+
+ public List<Conversation> getConversations() {
+ if (this.conversations == null) {
+ Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
+ for (Account account : this.accounts) {
+ accountLookupTable.put(account.getUuid(), account);
+ }
+ this.conversations = databaseBackend
+ .getConversations(Conversation.STATUS_AVAILABLE);
+ for (Conversation conv : this.conversations) {
+ Account account = accountLookupTable.get(conv.getAccountUuid());
+ conv.setAccount(account);
+ conv.setContact(findContact(account, conv.getContactJid()));
+ conv.setMessages(databaseBackend.getMessages(conv, 50));
+ }
+ }
+ return this.conversations;
+ }
+
+ public List<Account> getAccounts() {
+ return this.accounts;
+ }
+
+ public Contact findContact(Account account, String jid) {
+ Contact contact = databaseBackend.findContact(account, jid);
+ if (contact != null) {
+ contact.setAccount(account);
+ }
+ return contact;
+ }
+
+ public Conversation findOrCreateConversation(Account account, String jid,
+ boolean muc) {
+ for (Conversation conv : this.getConversations()) {
+ if ((conv.getAccount().equals(account))
+ && (conv.getContactJid().equals(jid))) {
+ return conv;
+ }
+ }
+ Conversation conversation = databaseBackend.findConversation(account,
+ jid);
+ if (conversation != null) {
+ conversation.setStatus(Conversation.STATUS_AVAILABLE);
+ conversation.setAccount(account);
+ if (muc) {
+ conversation.setMode(Conversation.MODE_MULTI);
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ joinMuc(conversation);
+ }
+ } else {
+ conversation.setMode(Conversation.MODE_SINGLE);
+ }
+ this.databaseBackend.updateConversation(conversation);
+ conversation.setContact(findContact(account,
+ conversation.getContactJid()));
+ } else {
+ String conversationName;
+ Contact contact = findContact(account, jid);
+ if (contact != null) {
+ conversationName = contact.getDisplayName();
+ } else {
+ conversationName = jid.split("@")[0];
+ }
+ if (muc) {
+ conversation = new Conversation(conversationName, account, jid,
+ Conversation.MODE_MULTI);
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ joinMuc(conversation);
+ }
+ } else {
+ conversation = new Conversation(conversationName, account, jid,
+ Conversation.MODE_SINGLE);
+ }
+ conversation.setContact(contact);
+ this.databaseBackend.createConversation(conversation);
+ }
+ this.conversations.add(conversation);
+ if (this.convChangedListener != null) {
+ this.convChangedListener.onConversationListChanged();
+ }
+ return conversation;
+ }
+
+ public void archiveConversation(Conversation conversation) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation);
+ } else {
+ try {
+ conversation.endOtrIfNeeded();
+ } catch (OtrException e) {
+ Log.d(LOGTAG,
+ "error ending otr session for "
+ + conversation.getName());
+ }
+ }
+ this.databaseBackend.updateConversation(conversation);
+ this.conversations.remove(conversation);
+ if (this.convChangedListener != null) {
+ this.convChangedListener.onConversationListChanged();
+ }
+ }
+
+ public int getConversationCount() {
+ return this.databaseBackend.getConversationCount();
+ }
+
+ public void createAccount(Account account) {
+ databaseBackend.createAccount(account);
+ this.accounts.add(account);
+ account.setXmppConnection(this.createConnection(account));
+ if (accountChangedListener != null)
+ accountChangedListener.onAccountListChangedListener();
+ }
+
+ public void deleteContact(Contact contact) {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ Element query = new Element("query");
+ query.setAttribute("xmlns", "jabber:iq:roster");
+ Element item = new Element("item");
+ item.setAttribute("jid", contact.getJid());
+ item.setAttribute("subscription", "remove");
+ query.addChild(item);
+ iq.addChild(query);
+ contact.getAccount().getXmppConnection().sendIqPacket(iq, null);
+ replaceContactInConversation(contact.getJid(), null);
+ databaseBackend.deleteContact(contact);
+ }
+
+ public void updateAccount(Account account) {
+ databaseBackend.updateAccount(account);
+ if (account.getXmppConnection() != null) {
+ disconnect(account);
+ }
+ if (!account.isOptionSet(Account.OPTION_DISABLED)) {
+ account.setXmppConnection(this.createConnection(account));
+ }
+ if (accountChangedListener != null)
+ accountChangedListener.onAccountListChangedListener();
+ }
+
+ public void deleteAccount(Account account) {
+ Log.d(LOGTAG, "called delete account");
+ if (account.getXmppConnection() != null) {
+ this.disconnect(account);
+ }
+ databaseBackend.deleteAccount(account);
+ this.accounts.remove(account);
+ if (accountChangedListener != null)
+ accountChangedListener.onAccountListChangedListener();
+ }
+
+ public void setOnConversationListChangedListener(
+ OnConversationListChangedListener listener) {
+ this.convChangedListener = listener;
+ }
+
+ public void removeOnConversationListChangedListener() {
+ this.convChangedListener = null;
+ }
+
+ public void setOnAccountListChangedListener(
+ OnAccountListChangedListener listener) {
+ this.accountChangedListener = listener;
+ }
+
+ public void removeOnAccountListChangedListener() {
+ this.accountChangedListener = null;
+ }
+
+ public void connectMultiModeConversations(Account account) {
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); i++) {
+ Conversation conversation = conversations.get(i);
+ if ((conversation.getMode() == Conversation.MODE_MULTI)
+ && (conversation.getAccount() == account)) {
+ joinMuc(conversation);
+ }
+ }
+ }
+
+ public void joinMuc(Conversation conversation) {
+ String muc = conversation.getContactJid();
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("to", muc + "/"
+ + conversation.getAccount().getUsername());
+ Element x = new Element("x");
+ x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
+ if (conversation.getMessages().size() != 0) {
+ Element history = new Element("history");
+ long lastMsgTime = conversation.getLatestMessage().getTimeSent();
+ long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
+ history.setAttribute("seconds", diff + "");
+ x.addChild(history);
+ }
+ packet.addChild(x);
+ conversation.getAccount().getXmppConnection()
+ .sendPresencePacket(packet);
+ }
+
+ public void leaveMuc(Conversation conversation) {
+
+ }
+
+ public void disconnect(Account account) {
+ List<Conversation> conversations = getConversations();
+ for (int i = 0; i < conversations.size(); i++) {
+ Conversation conversation = conversations.get(i);
+ if (conversation.getAccount() == account) {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ leaveMuc(conversation);
+ } else {
+ try {
+ conversation.endOtrIfNeeded();
+ } catch (OtrException e) {
+ Log.d(LOGTAG, "error ending otr session for "
+ + conversation.getName());
+ }
+ }
+ }
+ }
+ account.getXmppConnection().disconnect();
+ Log.d(LOGTAG, "disconnected account: " + account.getJid());
+ account.setXmppConnection(null);
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ public void updateContact(Contact contact) {
+ databaseBackend.updateContact(contact);
+ }
+
+ public void updateMessage(Message message) {
+ databaseBackend.updateMessage(message);
+ }
+
+ public void createContact(Contact contact) {
+ SharedPreferences sharedPref = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+ boolean autoGrant = sharedPref.getBoolean("grant_new_contacts", true);
+ if (autoGrant) {
+ contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
+ contact.setSubscriptionOption(Contact.Subscription.ASKING);
+ }
+ databaseBackend.createContact(contact);
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ Element query = new Element("query");
+ query.setAttribute("xmlns", "jabber:iq:roster");
+ Element item = new Element("item");
+ item.setAttribute("jid", contact.getJid());
+ item.setAttribute("name", contact.getJid());
+ query.addChild(item);
+ iq.addChild(query);
+ Account account = contact.getAccount();
+ account.getXmppConnection().sendIqPacket(iq, null);
+ if (autoGrant) {
+ requestPresenceUpdatesFrom(contact);
+ }
+ replaceContactInConversation(contact.getJid(), contact);
+ }
+
+ public void requestPresenceUpdatesFrom(Contact contact) {
+ // Requesting a Subscription type=subscribe
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("type", "subscribe");
+ packet.setAttribute("to", contact.getJid());
+ packet.setAttribute("from", contact.getAccount().getJid());
+ Log.d(LOGTAG, packet.toString());
+ contact.getAccount().getXmppConnection().sendPresencePacket(packet);
+ }
+
+ public void stopPresenceUpdatesFrom(Contact contact) {
+ // Unsubscribing type='unsubscribe'
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("type", "unsubscribe");
+ packet.setAttribute("to", contact.getJid());
+ packet.setAttribute("from", contact.getAccount().getJid());
+ Log.d(LOGTAG, packet.toString());
+ contact.getAccount().getXmppConnection().sendPresencePacket(packet);
+ }
+
+ public void stopPresenceUpdatesTo(Contact contact) {
+ // Canceling a Subscription type=unsubscribed
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("type", "unsubscribed");
+ packet.setAttribute("to", contact.getJid());
+ packet.setAttribute("from", contact.getAccount().getJid());
+ Log.d(LOGTAG, packet.toString());
+ contact.getAccount().getXmppConnection().sendPresencePacket(packet);
+ }
+
+ public void sendPresenceUpdatesTo(Contact contact) {
+ // type='subscribed'
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("type", "subscribed");
+ packet.setAttribute("to", contact.getJid());
+ packet.setAttribute("from", contact.getAccount().getJid());
+ Log.d(LOGTAG, packet.toString());
+ contact.getAccount().getXmppConnection().sendPresencePacket(packet);
+ }
+
+ public void sendPgpPresence(Account account, String signature) {
+ PresencePacket packet = new PresencePacket();
+ packet.setAttribute("from", account.getFullJid());
+ Element status = new Element("status");
+ status.setContent("online");
+ packet.addChild(status);
+ Element x = new Element("x");
+ x.setAttribute("xmlns", "jabber:x:signed");
+ x.setContent(signature);
+ packet.addChild(x);
+ account.getXmppConnection().sendPresencePacket(packet);
+ }
+
+ public void generatePgpAnnouncement(Account account)
+ throws PgpEngine.UserInputRequiredException {
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ String signature = getPgpEngine().generateSignature("online");
+ account.setKey("pgp_signature", signature);
+ databaseBackend.updateAccount(account);
+ sendPgpPresence(account, signature);
+ }
+ }
+} \ No newline at end of file
diff --git a/src/eu/siacs/conversations/ui/ConversationActivity.java b/src/eu/siacs/conversations/ui/ConversationActivity.java
new file mode 100644
index 00000000..7660b8fd
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/ConversationActivity.java
@@ -0,0 +1,481 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.utils.UIHelper;
+import android.net.Uri;
+import android.os.Bundle;
+import android.app.AlertDialog;
+import android.app.FragmentTransaction;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.graphics.Typeface;
+import android.support.v4.widget.SlidingPaneLayout;
+import android.support.v4.widget.SlidingPaneLayout.PanelSlideListener;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.PopupMenu;
+import android.widget.PopupMenu.OnMenuItemClickListener;
+import android.widget.TextView;
+import android.widget.ImageView;
+
+public class ConversationActivity extends XmppActivity {
+
+ public static final String VIEW_CONVERSATION = "viewConversation";
+ public static final String CONVERSATION = "conversationUuid";
+
+ public static final int REQUEST_SEND_MESSAGE = 0x75441;
+ public static final int REQUEST_DECRYPT_PGP = 0x76783;
+
+ protected SlidingPaneLayout spl;
+
+ private List<Conversation> conversationList = new ArrayList<Conversation>();
+ private Conversation selectedConversation = null;
+ private ListView listView;
+
+ private boolean paneShouldBeOpen = true;
+ private ArrayAdapter<Conversation> listAdapter;
+
+ private OnConversationListChangedListener onConvChanged = new OnConversationListChangedListener() {
+
+ @Override
+ public void onConversationListChanged() {
+ conversationList.clear();
+ conversationList.addAll(xmppConnectionService
+ .getConversations());
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ updateConversationList();
+ if(paneShouldBeOpen) {
+ if (conversationList.size() >= 1) {
+ swapConversationFragment();
+ } else {
+ startActivity(new Intent(getApplicationContext(), NewConversationActivity.class));
+ finish();
+ }
+ }
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation");
+ if (selectedFragment!=null) {
+ selectedFragment.updateMessages();
+ }
+ }
+ });
+ }
+ };
+
+ private DialogInterface.OnClickListener addToRoster = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String jid = getSelectedConversation().getContactJid();
+ Account account = getSelectedConversation().getAccount();
+ String name = jid.split("@")[0];
+ Contact contact = new Contact(account, name, jid, null);
+ xmppConnectionService.createContact(contact);
+ }
+ };
+
+ public List<Conversation> getConversationList() {
+ return this.conversationList;
+ }
+
+ public Conversation getSelectedConversation() {
+ return this.selectedConversation;
+ }
+
+ public ListView getConversationListView() {
+ return this.listView;
+ }
+
+ public SlidingPaneLayout getSlidingPaneLayout() {
+ return this.spl;
+ }
+
+ public boolean shouldPaneBeOpen() {
+ return paneShouldBeOpen;
+ }
+
+ public void updateConversationList() {
+ if (conversationList.size() >= 1) {
+ Collections.sort(this.conversationList, new Comparator<Conversation>() {
+ @Override
+ public int compare(Conversation lhs, Conversation rhs) {
+ return (int) (rhs.getLatestMessage().getTimeSent() - lhs.getLatestMessage().getTimeSent());
+ }
+ });
+ }
+ this.listView.invalidateViews();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.fragment_conversations_overview);
+
+ listView = (ListView) findViewById(R.id.list);
+
+ this.listAdapter = new ArrayAdapter<Conversation>(this,
+ R.layout.conversation_list_row, conversationList) {
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ if (view == null) {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = (View) inflater.inflate(
+ R.layout.conversation_list_row, null);
+ }
+ Conversation conv = getItem(position);
+ TextView convName = (TextView) view.findViewById(R.id.conversation_name);
+ convName.setText(conv.getName());
+ TextView convLastMsg = (TextView) view.findViewById(R.id.conversation_lastmsg);
+ convLastMsg.setText(conv.getLatestMessage().getBody());
+
+ if(!conv.isRead()) {
+ convName.setTypeface(null,Typeface.BOLD);
+ convLastMsg.setTypeface(null,Typeface.BOLD);
+ } else {
+ convName.setTypeface(null,Typeface.NORMAL);
+ convLastMsg.setTypeface(null,Typeface.NORMAL);
+ }
+
+ ((TextView) view.findViewById(R.id.conversation_lastupdate))
+ .setText(UIHelper.readableTimeDifference(getItem(position).getLatestMessage().getTimeSent()));
+
+ Uri profilePhoto = getItem(position).getProfilePhotoUri();
+ ImageView imageView = (ImageView) view.findViewById(R.id.conversation_image);
+ if (profilePhoto!=null) {
+ imageView.setImageURI(profilePhoto);
+ } else {
+ imageView.setImageBitmap(UIHelper.getUnknownContactPicture(getItem(position).getName(),200));
+ }
+
+
+ ((ImageView) view.findViewById(R.id.conversation_image))
+ .setImageURI(getItem(position).getProfilePhotoUri());
+ return view;
+ }
+
+ };
+
+ listView.setAdapter(this.listAdapter);
+
+ listView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View clickedView,
+ int position, long arg3) {
+ paneShouldBeOpen = false;
+ if (selectedConversation != conversationList.get(position)) {
+ selectedConversation = conversationList.get(position);
+ swapConversationFragment(); //.onBackendConnected(conversationList.get(position));
+ } else {
+ spl.closePane();
+ }
+ }
+ });
+ spl = (SlidingPaneLayout) findViewById(R.id.slidingpanelayout);
+ spl.setParallaxDistance(150);
+ spl.setShadowResource(R.drawable.es_slidingpane_shadow);
+ spl.setSliderFadeColor(0);
+ spl.setPanelSlideListener(new PanelSlideListener() {
+
+ @Override
+ public void onPanelOpened(View arg0) {
+ paneShouldBeOpen = true;
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setTitle(R.string.app_name);
+ invalidateOptionsMenu();
+
+ InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
+
+ View focus = getCurrentFocus();
+
+ if (focus != null) {
+
+ inputManager.hideSoftInputFromWindow(
+ focus.getWindowToken(),
+ InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+
+ @Override
+ public void onPanelClosed(View arg0) {
+ paneShouldBeOpen = false;
+ if (conversationList.size() > 0) {
+ getActionBar().setDisplayHomeAsUpEnabled(true);
+ getActionBar().setTitle(getSelectedConversation().getName());
+ invalidateOptionsMenu();
+ if (!getSelectedConversation().isRead()) {
+ getSelectedConversation().markRead();
+ updateConversationList();
+ }
+ }
+ }
+
+ @Override
+ public void onPanelSlide(View arg0, float arg1) {
+ // TODO Auto-generated method stub
+
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.conversations, menu);
+ MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security);
+ MenuItem menuArchive = (MenuItem) menu.findItem(R.id.action_archive);
+ MenuItem menuMucDetails = (MenuItem) menu.findItem(R.id.action_muc_details);
+ MenuItem menuContactDetails = (MenuItem) menu.findItem(R.id.action_contact_details);
+
+ if (spl.isOpen()) {
+ menuArchive.setVisible(false);
+ menuMucDetails.setVisible(false);
+ menuContactDetails.setVisible(false);
+ menuSecure.setVisible(false);
+ } else {
+ ((MenuItem) menu.findItem(R.id.action_add)).setVisible(false);
+ if (this.getSelectedConversation()!=null) {
+ if (this.getSelectedConversation().getMode() == Conversation.MODE_MULTI) {
+ menuMucDetails.setVisible(true);
+ menuContactDetails.setVisible(false);
+ menuSecure.setVisible(false);
+ menuArchive.setTitle("Leave conference");
+ } else {
+ menuContactDetails.setVisible(true);
+ menuMucDetails.setVisible(false);
+ if (this.getSelectedConversation().getLatestMessage().getEncryption() != Message.ENCRYPTION_NONE) {
+ menuSecure.setIcon(R.drawable.ic_action_secure);
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ spl.openPane();
+ break;
+ case R.id.action_settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ break;
+ case R.id.action_accounts:
+ startActivity(new Intent(this, ManageAccountActivity.class));
+ break;
+ case R.id.action_add:
+ startActivity(new Intent(this, NewConversationActivity.class));
+ break;
+ case R.id.action_archive:
+ Conversation conv = getSelectedConversation();
+ conv.setStatus(Conversation.STATUS_ARCHIVED);
+ paneShouldBeOpen = true;
+ spl.openPane();
+ xmppConnectionService.archiveConversation(conv);
+ selectedConversation = conversationList.get(0);
+ break;
+ case R.id.action_contact_details:
+ DialogContactDetails details = new DialogContactDetails();
+ Contact contact = this.getSelectedConversation().getContact();
+ if (contact != null) {
+ contact.setAccount(this.selectedConversation.getAccount());
+ details.setContact(contact);
+ details.show(getFragmentManager(), "details");
+ } else {
+ String jid = getSelectedConversation().getContactJid();
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(jid);
+ builder.setMessage("The contact is not in your roster. Would you like to add it.");
+ builder.setNegativeButton("Cancel", null);
+ builder.setPositiveButton("Add",addToRoster);
+ builder.create().show();
+ }
+ break;
+ case R.id.action_security:
+ final Conversation selConv = getSelectedConversation();
+ View menuItemView = findViewById(R.id.action_security);
+ PopupMenu popup = new PopupMenu(this, menuItemView);
+ final ConversationFragment fragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation");
+ if (fragment!=null) {
+ popup.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.encryption_choice_none:
+ selConv.nextMessageEncryption = Message.ENCRYPTION_NONE;
+ item.setChecked(true);
+ break;
+ case R.id.encryption_choice_otr:
+ selConv.nextMessageEncryption = Message.ENCRYPTION_OTR;
+ item.setChecked(true);
+ break;
+ case R.id.encryption_choice_pgp:
+ selConv.nextMessageEncryption = Message.ENCRYPTION_PGP;
+ item.setChecked(true);
+ break;
+ default:
+ selConv.nextMessageEncryption = Message.ENCRYPTION_NONE;
+ break;
+ }
+ fragment.updateChatMsgHint();
+ return true;
+ }
+ });
+ popup.inflate(R.menu.encryption_choices);
+ switch (selConv.nextMessageEncryption) {
+ case Message.ENCRYPTION_NONE:
+ popup.getMenu().findItem(R.id.encryption_choice_none).setChecked(true);
+ break;
+ case Message.ENCRYPTION_OTR:
+ popup.getMenu().findItem(R.id.encryption_choice_otr).setChecked(true);
+ break;
+ case Message.ENCRYPTION_PGP:
+ popup.getMenu().findItem(R.id.encryption_choice_pgp).setChecked(true);
+ break;
+ case Message.ENCRYPTION_DECRYPTED:
+ popup.getMenu().findItem(R.id.encryption_choice_pgp).setChecked(true);
+ break;
+ default:
+ popup.getMenu().findItem(R.id.encryption_choice_none).setChecked(true);
+ break;
+ }
+ popup.show();
+ }
+
+ break;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ protected ConversationFragment swapConversationFragment() {
+ ConversationFragment selectedFragment = new ConversationFragment();
+
+ FragmentTransaction transaction = getFragmentManager()
+ .beginTransaction();
+ transaction.replace(R.id.selected_conversation, selectedFragment,"conversation");
+ transaction.commit();
+ return selectedFragment;
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ if (!spl.isOpen()) {
+ spl.openPane();
+ return false;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ public void onStart() {
+ super.onStart();
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancelAll();
+ if (conversationList.size()>=1) {
+ onConvChanged.onConversationListChanged();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ Log.d("gultsch","called on stop in conversation activity");
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnConversationListChangedListener();
+ }
+ super.onStop();
+ }
+
+ @Override
+ void onBackendConnected() {
+
+ xmppConnectionService.setOnConversationListChangedListener(this.onConvChanged);
+
+ if (conversationList.size()==0) {
+ conversationList.clear();
+ conversationList.addAll(xmppConnectionService
+ .getConversations());
+
+ this.updateConversationList();
+ }
+
+ if ((getIntent().getAction().equals(Intent.ACTION_VIEW) && (!handledViewIntent))) {
+ if (getIntent().getType().equals(
+ ConversationActivity.VIEW_CONVERSATION)) {
+ handledViewIntent = true;
+
+ String convToView = (String) getIntent().getExtras().get(CONVERSATION);
+
+ for(int i = 0; i < conversationList.size(); ++i) {
+ if (conversationList.get(i).getUuid().equals(convToView)) {
+ selectedConversation = conversationList.get(i);
+ }
+ }
+ paneShouldBeOpen = false;
+ swapConversationFragment();
+ }
+ } else {
+ if (xmppConnectionService.getAccounts().size() == 0) {
+ startActivity(new Intent(this, ManageAccountActivity.class));
+ finish();
+ } else if (conversationList.size() <= 0) {
+ //add no history
+ startActivity(new Intent(this, NewConversationActivity.class));
+ finish();
+ } else {
+ spl.openPane();
+ //find currently loaded fragment
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation");
+ if (selectedFragment!=null) {
+ Log.d("gultsch","ConversationActivity. found old fragment.");
+ selectedFragment.onBackendConnected();
+ } else {
+ Log.d("gultsch","conversationactivity. no old fragment found. creating new one");
+ selectedConversation = conversationList.get(0);
+ Log.d("gultsch","selected conversation is #"+selectedConversation);
+ swapConversationFragment();
+ }
+ }
+ }
+ }
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_DECRYPT_PGP) {
+ ConversationFragment selectedFragment = (ConversationFragment) getFragmentManager().findFragmentByTag("conversation");
+ if (selectedFragment!=null) {
+ selectedFragment.hidePgpPassphraseBox();
+ }
+ }
+ }
+ }
+}
diff --git a/src/eu/siacs/conversations/ui/ConversationFragment.java b/src/eu/siacs/conversations/ui/ConversationFragment.java
new file mode 100644
index 00000000..ff06fafc
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/ConversationFragment.java
@@ -0,0 +1,602 @@
+package eu.siacs.conversations.ui;
+
+import java.io.FileNotFoundException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+import net.java.otr4j.session.SessionStatus;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine.OpenPgpException;
+import eu.siacs.conversations.crypto.PgpEngine.UserInputRequiredException;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.utils.PhoneHelper;
+import eu.siacs.conversations.utils.UIHelper;
+import android.app.AlertDialog;
+import android.app.Fragment;
+import android.content.DialogInterface;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.content.IntentSender.SendIntentException;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Typeface;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ListView;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+public class ConversationFragment extends Fragment {
+
+ protected Conversation conversation;
+ protected ListView messagesView;
+ protected LayoutInflater inflater;
+ protected List<Message> messageList = new ArrayList<Message>();
+ protected ArrayAdapter<Message> messageListAdapter;
+ protected Contact contact;
+ protected BitmapCache mBitmapCache = new BitmapCache();
+
+ protected String queuedPqpMessage = null;
+
+ private EditText chatMsg;
+
+ protected Bitmap selfBitmap;
+
+ private IntentSender askForPassphraseIntent = null;
+
+ private OnClickListener sendMsgListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ if (chatMsg.getText().length() < 1)
+ return;
+ Message message = new Message(conversation, chatMsg.getText()
+ .toString(), conversation.nextMessageEncryption);
+ if (conversation.nextMessageEncryption == Message.ENCRYPTION_OTR) {
+ sendOtrMessage(message);
+ } else if (conversation.nextMessageEncryption == Message.ENCRYPTION_PGP) {
+ sendPgpMessage(message);
+ } else {
+ sendPlainTextMessage(message);
+ }
+ }
+ };
+ protected OnClickListener clickToDecryptListener = new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ Log.d("gultsch","clicked to decrypt");
+ if (askForPassphraseIntent!=null) {
+ try {
+ getActivity().startIntentSenderForResult(askForPassphraseIntent, ConversationActivity.REQUEST_DECRYPT_PGP, null, 0, 0, 0);
+ } catch (SendIntentException e) {
+ Log.d("gultsch","couldnt fire intent");
+ }
+ }
+ }
+ };
+ private LinearLayout pgpInfo;
+
+ public void hidePgpPassphraseBox() {
+ pgpInfo.setVisibility(View.GONE);
+ }
+
+ public void updateChatMsgHint() {
+ if (conversation.getMode() == Conversation.MODE_MULTI) {
+ chatMsg.setHint("Send message to conference");
+ } else {
+ switch (conversation.nextMessageEncryption) {
+ case Message.ENCRYPTION_NONE:
+ chatMsg.setHint("Send plain text message");
+ break;
+ case Message.ENCRYPTION_OTR:
+ chatMsg.setHint("Send OTR encrypted message");
+ break;
+ case Message.ENCRYPTION_PGP:
+ chatMsg.setHint("Send openPGP encryted messeage");
+ break;
+ case Message.ENCRYPTION_DECRYPTED:
+ chatMsg.setHint("Send openPGP encryted messeage");
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(final LayoutInflater inflater,
+ ViewGroup container, Bundle savedInstanceState) {
+
+ this.inflater = inflater;
+
+ final View view = inflater.inflate(R.layout.fragment_conversation,
+ container, false);
+ chatMsg = (EditText) view.findViewById(R.id.textinput);
+ ImageButton sendButton = (ImageButton) view
+ .findViewById(R.id.textSendButton);
+ sendButton.setOnClickListener(this.sendMsgListener);
+
+ pgpInfo = (LinearLayout) view.findViewById(R.id.pgp_keyentry);
+ pgpInfo.setOnClickListener(clickToDecryptListener);
+
+ messagesView = (ListView) view.findViewById(R.id.messages_view);
+
+ messageListAdapter = new ArrayAdapter<Message>(this.getActivity()
+ .getApplicationContext(), R.layout.message_sent,
+ this.messageList) {
+
+ private static final int SENT = 0;
+ private static final int RECIEVED = 1;
+ private static final int ERROR = 2;
+
+ @Override
+ public int getViewTypeCount() {
+ return 3;
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (getItem(position).getStatus() == Message.STATUS_RECIEVED) {
+ return RECIEVED;
+ } else if (getItem(position).getStatus() == Message.STATUS_ERROR) {
+ return ERROR;
+ } else {
+ return SENT;
+ }
+ }
+
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ Message item = getItem(position);
+ int type = getItemViewType(position);
+ ViewHolder viewHolder;
+ if (view == null) {
+ viewHolder = new ViewHolder();
+ switch (type) {
+ case SENT:
+ view = (View) inflater.inflate(R.layout.message_sent,
+ null);
+ viewHolder.imageView = (ImageView) view
+ .findViewById(R.id.message_photo);
+ viewHolder.imageView.setImageBitmap(selfBitmap);
+ break;
+ case RECIEVED:
+ view = (View) inflater.inflate(
+ R.layout.message_recieved, null);
+ viewHolder.imageView = (ImageView) view
+ .findViewById(R.id.message_photo);
+ if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
+ Uri uri = item.getConversation()
+ .getProfilePhotoUri();
+ if (uri != null) {
+ viewHolder.imageView
+ .setImageBitmap(mBitmapCache.get(item
+ .getConversation().getName(),
+ uri));
+ } else {
+ viewHolder.imageView
+ .setImageBitmap(mBitmapCache.get(item
+ .getConversation().getName(),
+ null));
+ }
+ }
+ break;
+ case ERROR:
+ view = (View) inflater.inflate(R.layout.message_error,
+ null);
+ viewHolder.imageView = (ImageView) view
+ .findViewById(R.id.message_photo);
+ viewHolder.imageView.setImageBitmap(mBitmapCache
+ .getError());
+ break;
+ default:
+ viewHolder = null;
+ break;
+ }
+ viewHolder.messageBody = (TextView) view
+ .findViewById(R.id.message_body);
+ viewHolder.time = (TextView) view
+ .findViewById(R.id.message_time);
+ view.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) view.getTag();
+ }
+ if (type == RECIEVED) {
+ if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
+ if (item.getCounterpart() != null) {
+ viewHolder.imageView.setImageBitmap(mBitmapCache
+ .get(item.getCounterpart(), null));
+ } else {
+ viewHolder.imageView
+ .setImageBitmap(mBitmapCache.get(item
+ .getConversation().getName(), null));
+ }
+ }
+ }
+ String body = item.getBody();
+ if (body != null) {
+ if (item.getEncryption() == Message.ENCRYPTION_PGP) {
+ viewHolder.messageBody.setText(getString(R.string.encrypted_message));
+ viewHolder.messageBody.setTextColor(0xff33B5E5);
+ viewHolder.messageBody.setTypeface(null,Typeface.ITALIC);
+ } else {
+ viewHolder.messageBody.setText(body.trim());
+ viewHolder.messageBody.setTextColor(0xff000000);
+ viewHolder.messageBody.setTypeface(null, Typeface.NORMAL);
+ }
+ }
+ if (item.getStatus() == Message.STATUS_UNSEND) {
+ viewHolder.time.setTypeface(null, Typeface.ITALIC);
+ viewHolder.time.setText("sending\u2026");
+ } else {
+ viewHolder.time.setTypeface(null, Typeface.NORMAL);
+ if ((item.getConversation().getMode() == Conversation.MODE_SINGLE)
+ || (type != RECIEVED)) {
+ viewHolder.time.setText(UIHelper
+ .readableTimeDifference(item.getTimeSent()));
+ } else {
+ viewHolder.time.setText(item.getCounterpart()
+ + " \u00B7 "
+ + UIHelper.readableTimeDifference(item
+ .getTimeSent()));
+ }
+ }
+ return view;
+ }
+ };
+ messagesView.setAdapter(messageListAdapter);
+
+ return view;
+ }
+
+ protected Bitmap findSelfPicture() {
+ SharedPreferences sharedPref = PreferenceManager
+ .getDefaultSharedPreferences(getActivity()
+ .getApplicationContext());
+ boolean showPhoneSelfContactPicture = sharedPref.getBoolean(
+ "show_phone_selfcontact_picture", true);
+
+ Bitmap self = null;
+
+ if (showPhoneSelfContactPicture) {
+ Uri selfiUri = PhoneHelper.getSefliUri(getActivity());
+ if (selfiUri != null) {
+ try {
+ self = BitmapFactory.decodeStream(getActivity()
+ .getContentResolver().openInputStream(selfiUri));
+ } catch (FileNotFoundException e) {
+ self = null;
+ }
+ }
+ }
+ if (self == null) {
+ self = UIHelper.getUnknownContactPicture(conversation.getAccount()
+ .getJid(), 200);
+ }
+
+ final Bitmap selfBitmap = self;
+ return selfBitmap;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ ConversationActivity activity = (ConversationActivity) getActivity();
+
+ if (activity.xmppConnectionServiceBound) {
+ this.onBackendConnected();
+ }
+ }
+
+ public void onBackendConnected() {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ this.conversation = activity.getSelectedConversation();
+ this.selfBitmap = findSelfPicture();
+ updateMessages();
+ // rendering complete. now go tell activity to close pane
+ if (!activity.shouldPaneBeOpen()) {
+ activity.getSlidingPaneLayout().closePane();
+ activity.getActionBar().setDisplayHomeAsUpEnabled(true);
+ activity.getActionBar().setTitle(conversation.getName());
+ activity.invalidateOptionsMenu();
+ if (!conversation.isRead()) {
+ conversation.markRead();
+ activity.updateConversationList();
+ }
+ }
+ if (queuedPqpMessage != null) {
+ this.conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
+ Message message = new Message(conversation, queuedPqpMessage,
+ Message.ENCRYPTION_PGP);
+ sendPgpMessage(message);
+ }
+ }
+
+ public void updateMessages() {
+ ConversationActivity activity = (ConversationActivity) getActivity();
+ List<Message> encryptedMessages = new LinkedList<Message>();
+ for(Message message : this.conversation.getMessages()) {
+ if (message.getEncryption() == Message.ENCRYPTION_PGP) {
+ encryptedMessages.add(message);
+ }
+ }
+ if (encryptedMessages.size() > 0) {
+ DecryptMessage task = new DecryptMessage();
+ Message[] msgs = new Message[encryptedMessages.size()];
+ task.execute(encryptedMessages.toArray(msgs));
+ }
+ this.messageList.clear();
+ this.messageList.addAll(this.conversation.getMessages());
+ this.messageListAdapter.notifyDataSetChanged();
+ if (messageList.size() >= 1) {
+ int latestEncryption = this.conversation.getLatestMessage()
+ .getEncryption();
+ if (latestEncryption== Message.ENCRYPTION_DECRYPTED) {
+ conversation.nextMessageEncryption = Message.ENCRYPTION_PGP;
+ } else {
+ conversation.nextMessageEncryption = latestEncryption;
+ }
+ makeFingerprintWarning(latestEncryption);
+ }
+ getActivity().invalidateOptionsMenu();
+ updateChatMsgHint();
+ int size = this.messageList.size();
+ if (size >= 1)
+ messagesView.setSelection(size - 1);
+ if (!activity.shouldPaneBeOpen()) {
+ conversation.markRead();
+ activity.updateConversationList();
+ }
+ }
+
+ protected void makeFingerprintWarning(int latestEncryption) {
+ final LinearLayout fingerprintWarning = (LinearLayout) getView()
+ .findViewById(R.id.new_fingerprint);
+ if (conversation.getContact() != null) {
+ Set<String> knownFingerprints = conversation.getContact()
+ .getOtrFingerprints();
+ if ((latestEncryption == Message.ENCRYPTION_OTR)
+ && (conversation.hasValidOtrSession()
+ && (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
+ .contains(conversation.getOtrFingerprint())))) {
+ fingerprintWarning.setVisibility(View.VISIBLE);
+ TextView fingerprint = (TextView) getView().findViewById(
+ R.id.otr_fingerprint);
+ fingerprint.setText(conversation.getOtrFingerprint());
+ fingerprintWarning.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ AlertDialog dialog = UIHelper
+ .getVerifyFingerprintDialog(
+ (ConversationActivity) getActivity(),
+ conversation, fingerprintWarning);
+ dialog.show();
+ }
+ });
+ } else {
+ fingerprintWarning.setVisibility(View.GONE);
+ }
+ } else {
+ fingerprintWarning.setVisibility(View.GONE);
+ }
+ }
+
+ protected void sendPlainTextMessage(Message message) {
+ ConversationActivity activity = (ConversationActivity) getActivity();
+ activity.xmppConnectionService.sendMessage(message, null);
+ chatMsg.setText("");
+ }
+
+ protected void sendPgpMessage(final Message message) {
+ ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ Contact contact = message.getConversation().getContact();
+ if (contact.getPgpKeyId() != 0) {
+ xmppService.sendMessage(message, null);
+ chatMsg.setText("");
+ } else {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle("No openPGP key found");
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage("There is no openPGP key assoziated with this contact");
+ builder.setNegativeButton("Cancel", null);
+ builder.setPositiveButton("Send plain text",
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
+ message.setEncryption(Message.ENCRYPTION_NONE);
+ xmppService.sendMessage(message, null);
+ chatMsg.setText("");
+ }
+ });
+ builder.create().show();
+ }
+ }
+
+ public void resendPgpMessage(String msg) {
+ this.queuedPqpMessage = msg;
+ }
+
+ protected void sendOtrMessage(final Message message) {
+ ConversationActivity activity = (ConversationActivity) getActivity();
+ final XmppConnectionService xmppService = activity.xmppConnectionService;
+ if (conversation.hasValidOtrSession()) {
+ activity.xmppConnectionService.sendMessage(message, null);
+ chatMsg.setText("");
+ } else {
+ Hashtable<String, Integer> presences;
+ if (conversation.getContact() != null) {
+ presences = conversation.getContact().getPresences();
+ } else {
+ presences = null;
+ }
+ if ((presences == null) || (presences.size() == 0)) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ getActivity());
+ builder.setTitle("Contact is offline");
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage("Sending OTR encrypted messages to an offline contact is impossible.");
+ builder.setPositiveButton("Send plain text",
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
+ message.setEncryption(Message.ENCRYPTION_NONE);
+ xmppService.sendMessage(message, null);
+ chatMsg.setText("");
+ }
+ });
+ builder.setNegativeButton("Cancel", null);
+ builder.create().show();
+ } else if (presences.size() == 1) {
+ xmppService.sendMessage(message, (String) presences.keySet()
+ .toArray()[0]);
+ chatMsg.setText("");
+ } else {
+ AlertDialog.Builder builder = new AlertDialog.Builder(
+ getActivity());
+ builder.setTitle("Choose Presence");
+ final String[] presencesArray = new String[presences.size()];
+ presences.keySet().toArray(presencesArray);
+ builder.setItems(presencesArray,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog,
+ int which) {
+ xmppService.sendMessage(message,
+ presencesArray[which]);
+ chatMsg.setText("");
+ }
+ });
+ builder.create().show();
+ }
+ }
+ }
+
+ private static class ViewHolder {
+
+ protected TextView time;
+ protected TextView messageBody;
+ protected ImageView imageView;
+
+ }
+
+ private class BitmapCache {
+ private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
+ private Bitmap error = null;
+
+ public Bitmap get(String name, Uri uri) {
+ if (bitmaps.containsKey(name)) {
+ return bitmaps.get(name);
+ } else {
+ Bitmap bm;
+ if (uri != null) {
+ try {
+ bm = BitmapFactory.decodeStream(getActivity()
+ .getContentResolver().openInputStream(uri));
+ } catch (FileNotFoundException e) {
+ bm = UIHelper.getUnknownContactPicture(name, 200);
+ }
+ } else {
+ bm = UIHelper.getUnknownContactPicture(name, 200);
+ }
+ bitmaps.put(name, bm);
+ return bm;
+ }
+ }
+
+ public Bitmap getError() {
+ if (error == null) {
+ error = UIHelper.getErrorPicture(200);
+ }
+ return error;
+ }
+ }
+
+ class DecryptMessage extends AsyncTask<Message, Void, Boolean> {
+
+ @Override
+ protected Boolean doInBackground(Message... params) {
+ final ConversationActivity activity = (ConversationActivity) getActivity();
+ askForPassphraseIntent = null;
+ for(int i = 0; i < params.length; ++i) {
+ if (params[i].getEncryption() == Message.ENCRYPTION_PGP) {
+ String body = params[i].getBody();
+ String decrypted = null;
+ try {
+ if (activity==null) {
+ return false;
+ }
+ Log.d("gultsch","calling to decrypt message id #"+params[i].getUuid());
+ decrypted = activity.xmppConnectionService.getPgpEngine().decrypt(body);
+ } catch (UserInputRequiredException e) {
+ askForPassphraseIntent = e.getPendingIntent().getIntentSender();
+ activity.runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ pgpInfo.setVisibility(View.VISIBLE);
+ }
+ });
+
+ return false;
+
+ } catch (OpenPgpException e) {
+ Log.d("gultsch","error decrypting pgp");
+ }
+ if (decrypted!=null) {
+ params[i].setBody(decrypted);
+ params[i].setEncryption(Message.ENCRYPTION_DECRYPTED);
+ activity.xmppConnectionService.updateMessage(params[i]);
+ }
+ if (activity!=null) {
+ activity.runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ messageListAdapter.notifyDataSetChanged();
+ }
+ });
+ }
+ }
+ if (activity!=null) {
+ activity.runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ activity.updateConversationList();
+ }
+ });
+ }
+ }
+ return true;
+ }
+
+ }
+}
diff --git a/src/eu/siacs/conversations/ui/DialogContactDetails.java b/src/eu/siacs/conversations/ui/DialogContactDetails.java
new file mode 100644
index 00000000..1210bc3c
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/DialogContactDetails.java
@@ -0,0 +1,218 @@
+package eu.siacs.conversations.ui;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Presences;
+import eu.siacs.conversations.utils.UIHelper;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.CheckBox;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+public class DialogContactDetails extends DialogFragment {
+
+ private Contact contact = null;
+ boolean displayingInRoster = false;
+
+ private DialogContactDetails mDetailsDialog = this;
+ private XmppActivity activity;
+
+ private CheckBox send;
+ private CheckBox receive;
+
+ private DialogInterface.OnClickListener askRemoveFromRoster = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle("Delete from roster");
+ builder.setMessage("Do you want to delete "+contact.getJid()+" from your roster. The conversation assoziated with this account will not be removed.");
+ builder.setNegativeButton("Cancel", null);
+ builder.setPositiveButton("Delete",removeFromRoster);
+ builder.create().show();
+ }
+ };
+
+ private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ activity.xmppConnectionService.deleteContact(contact);
+ mDetailsDialog.dismiss();
+ }
+ };
+
+ private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
+ intent.setType(Contacts.CONTENT_ITEM_TYPE);
+ intent.putExtra(Intents.Insert.IM_HANDLE,contact.getJid());
+ intent.putExtra(Intents.Insert.IM_PROTOCOL,CommonDataKinds.Im.PROTOCOL_JABBER);
+ intent.putExtra("finishActivityOnSaveCompleted", true);
+ getActivity().startActivityForResult(intent,0);
+ mDetailsDialog.dismiss();
+ }
+ };
+
+ private DialogInterface.OnClickListener updateSubscriptions = new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ boolean needsUpdating = false;
+ if (contact.getSubscriptionOption(Contact.Subscription.FROM)) {
+ if (!send.isChecked()) {
+ contact.resetSubscriptionOption(Contact.Subscription.FROM);
+ contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
+ activity.xmppConnectionService.stopPresenceUpdatesTo(contact);
+ needsUpdating=true;
+ }
+ } else {
+ if (contact.getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) {
+ if (!send.isChecked()) {
+ contact.resetSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
+ needsUpdating=true;
+ }
+ } else {
+ if (send.isChecked()) {
+ contact.setSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT);
+ needsUpdating=true;
+ }
+ }
+ }
+ if (contact.getSubscriptionOption(Contact.Subscription.TO)) {
+ if (!receive.isChecked()) {
+ contact.resetSubscriptionOption(Contact.Subscription.TO);
+ activity.xmppConnectionService.stopPresenceUpdatesFrom(contact);
+ needsUpdating=true;
+ }
+ } else {
+ if (contact.getSubscriptionOption(Contact.Subscription.ASKING)) {
+ if (!receive.isChecked()) {
+ contact.resetSubscriptionOption(Contact.Subscription.ASKING);
+ activity.xmppConnectionService.stopPresenceUpdatesFrom(contact);
+ needsUpdating=true;
+ }
+ } else {
+ if (receive.isChecked()) {
+ contact.setSubscriptionOption(Contact.Subscription.ASKING);
+ activity.xmppConnectionService.requestPresenceUpdatesFrom(contact);
+ needsUpdating=true;
+ }
+ }
+ }
+ if (needsUpdating) {
+ activity.xmppConnectionService.updateContact(contact);
+ }
+ }
+ };
+
+ public void setContact(Contact contact) {
+ this.contact = contact;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ this.activity = (XmppActivity) getActivity();
+ AlertDialog.Builder builder = new AlertDialog.Builder(this.activity);
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View view = inflater.inflate(R.layout.dialog_contact_details, null);
+ TextView contactJid = (TextView) view.findViewById(R.id.details_contactjid);
+ TextView accountJid = (TextView) view.findViewById(R.id.details_account);
+ TextView status = (TextView) view.findViewById(R.id.details_contactstatus);
+ send = (CheckBox) view.findViewById(R.id.details_send_presence);
+ receive = (CheckBox) view.findViewById(R.id.details_receive_presence);
+ //ImageView contactPhoto = (ImageView) view.findViewById(R.id.details_contact_picture);
+ QuickContactBadge badge = (QuickContactBadge) view.findViewById(R.id.details_contact_badge);
+
+ if (contact.getSubscriptionOption(Contact.Subscription.FROM)) {
+ send.setChecked(true);
+ } else {
+ send.setText("Preemptively grant subscription request");
+ if (contact.getSubscriptionOption(Contact.Subscription.PREEMPTIVE_GRANT)) {
+ send.setChecked(true);
+ } else {
+ send.setChecked(false);
+ }
+ }
+ if (contact.getSubscriptionOption(Contact.Subscription.TO)) {
+ receive.setChecked(true);
+ } else {
+ receive.setText("Request presence updates");
+ if (contact.getSubscriptionOption(Contact.Subscription.ASKING)) {
+ receive.setChecked(true);
+ } else {
+ receive.setChecked(false);
+ }
+ }
+
+ switch (contact.getMostAvailableStatus()) {
+ case Presences.CHAT:
+ status.setText("free to chat");
+ status.setTextColor(0xFF83b600);
+ break;
+ case Presences.ONLINE:
+ status.setText("online");
+ status.setTextColor(0xFF83b600);
+ break;
+ case Presences.AWAY:
+ status.setText("away");
+ status.setTextColor(0xFFffa713);
+ break;
+ case Presences.XA:
+ status.setText("extended away");
+ status.setTextColor(0xFFffa713);
+ break;
+ case Presences.DND:
+ status.setText("do not disturb");
+ status.setTextColor(0xFFe92727);
+ break;
+ case Presences.OFFLINE:
+ status.setText("offline");
+ status.setTextColor(0xFFe92727);
+ break;
+ default:
+ status.setText("offline");
+ status.setTextColor(0xFFe92727);
+ break;
+ }
+ contactJid.setText(contact.getJid());
+ accountJid.setText(contact.getAccount().getJid());
+
+ UIHelper.prepareContactBadge(getActivity(), badge, contact);
+
+ if (contact.getSystemAccount()==null) {
+ badge.setOnClickListener(new OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ builder.setTitle("Add to phone book");
+ builder.setMessage("Do you want to add "+contact.getJid()+" to your phones contact list?");
+ builder.setNegativeButton("Cancel", null);
+ builder.setPositiveButton("Add",addToPhonebook);
+ builder.create().show();
+ }
+ });
+ }
+
+ builder.setView(view);
+ builder.setTitle(contact.getDisplayName());
+
+ builder.setNeutralButton("Done", this.updateSubscriptions);
+ builder.setPositiveButton("Remove from roster", this.askRemoveFromRoster);
+ return builder.create();
+ }
+}
diff --git a/src/eu/siacs/conversations/ui/EditAccount.java b/src/eu/siacs/conversations/ui/EditAccount.java
new file mode 100644
index 00000000..3ec74174
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/EditAccount.java
@@ -0,0 +1,138 @@
+package eu.siacs.conversations.ui;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.Validator;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.EditText;
+import android.widget.TextView;
+
+public class EditAccount extends DialogFragment {
+
+ protected Account account;
+
+ public void setAccount(Account account) {
+ this.account = account;
+ }
+
+ public interface EditAccountListener {
+ public void onAccountEdited(Account account);
+ }
+
+ protected EditAccountListener listener = null;
+
+ public void setEditAccountListener(EditAccountListener listener) {
+ this.listener = listener;
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ LayoutInflater inflater = getActivity().getLayoutInflater();
+ View view = inflater.inflate(R.layout.edit_account_dialog, null);
+ final EditText jidText = (EditText) view.findViewById(R.id.account_jid);
+ final TextView confirmPwDesc = (TextView) view
+ .findViewById(R.id.account_confirm_password_desc);
+ CheckBox useTLS = (CheckBox) view.findViewById(R.id.account_usetls);
+
+ final EditText password = (EditText) view
+ .findViewById(R.id.account_password);
+ final EditText passwordConfirm = (EditText) view
+ .findViewById(R.id.account_password_confirm2);
+ final CheckBox registerAccount = (CheckBox) view
+ .findViewById(R.id.edit_account_register_new);
+
+ final String okButtonDesc;
+
+ if (account != null) {
+ builder.setTitle("Edit account");
+ registerAccount.setVisibility(View.GONE);
+ jidText.setText(account.getJid());
+ password.setText(account.getPassword());
+ okButtonDesc = "Edit";
+ if (account.isOptionSet(Account.OPTION_USETLS)) {
+ useTLS.setChecked(true);
+ } else {
+ useTLS.setChecked(false);
+ }
+ } else {
+ builder.setTitle("Add account");
+ okButtonDesc = "Add";
+ }
+
+ registerAccount
+ .setOnCheckedChangeListener(new OnCheckedChangeListener() {
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView,
+ boolean isChecked) {
+ AlertDialog d = (AlertDialog) getDialog();
+ Button positiveButton = (Button) d
+ .getButton(Dialog.BUTTON_POSITIVE);
+ if (isChecked) {
+ positiveButton.setText("Register");
+ passwordConfirm.setVisibility(View.VISIBLE);
+ confirmPwDesc.setVisibility(View.VISIBLE);
+ } else {
+ passwordConfirm.setVisibility(View.GONE);
+ positiveButton.setText("Add");
+ confirmPwDesc.setVisibility(View.GONE);
+ }
+ }
+ });
+
+ builder.setView(view);
+ builder.setNeutralButton("Cancel", null);
+ builder.setPositiveButton(okButtonDesc, null);
+ return builder.create();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ final AlertDialog d = (AlertDialog) getDialog();
+ Button positiveButton = (Button) d.getButton(Dialog.BUTTON_POSITIVE);
+ positiveButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ EditText jidEdit = (EditText) d.findViewById(R.id.account_jid);
+ String jid = jidEdit.getText().toString();
+ EditText passwordEdit = (EditText) d
+ .findViewById(R.id.account_password);
+ String password = passwordEdit.getText().toString();
+ CheckBox useTLS = (CheckBox) d.findViewById(R.id.account_usetls);
+ String username;
+ String server;
+ if (Validator.isValidJid(jid)) {
+ String[] parts = jid.split("@");
+ username = parts[0];
+ server = parts[1];
+ } else {
+ jidEdit.setError("Invalid Jabber ID");
+ return;
+ }
+ if (account != null) {
+ account.setPassword(password);
+ account.setUsername(username);
+ account.setServer(server);
+ } else {
+ account = new Account(username, server, password);
+ }
+ account.setOption(Account.OPTION_USETLS, useTLS.isChecked());
+ if (listener != null) {
+ listener.onAccountEdited(account);
+ d.dismiss();
+ }
+ }
+ });
+ }
+}
diff --git a/src/eu/siacs/conversations/ui/ManageAccountActivity.java b/src/eu/siacs/conversations/ui/ManageAccountActivity.java
new file mode 100644
index 00000000..22b82e77
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/ManageAccountActivity.java
@@ -0,0 +1,312 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.crypto.PgpEngine;
+import eu.siacs.conversations.crypto.PgpEngine.UserInputRequiredException;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.ui.EditAccount.EditAccountListener;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+public class ManageAccountActivity extends XmppActivity implements ActionMode.Callback {
+
+ public static final int REQUEST_ANNOUNCE_PGP = 0x73731;
+
+ protected boolean isActionMode = false;
+ protected ActionMode actionMode;
+ protected Account selectedAccountForActionMode = null;
+
+ protected List<Account> accountList = new ArrayList<Account>();
+ protected ListView accountListView;
+ protected ArrayAdapter<Account> accountListViewAdapter;
+ protected OnAccountListChangedListener accountChanged = new OnAccountListChangedListener() {
+
+ @Override
+ public void onAccountListChangedListener() {
+ Log.d("xmppService", "ui on account list changed listener");
+ accountList.clear();
+ accountList.addAll(xmppConnectionService.getAccounts());
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ if (accountList.size() == 1) {
+ startActivity(new Intent(getApplicationContext(),
+ NewConversationActivity.class));
+ }
+ accountListViewAdapter.notifyDataSetChanged();
+ }
+ });
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.manage_accounts);
+
+ accountListView = (ListView) findViewById(R.id.account_list);
+ accountListViewAdapter = new ArrayAdapter<Account>(
+ getApplicationContext(), R.layout.account_row, this.accountList) {
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ Account account = getItem(position);
+ if (view == null) {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ view = (View) inflater.inflate(R.layout.account_row, null);
+ }
+ ((TextView) view.findViewById(R.id.account_jid))
+ .setText(account.getJid());
+ TextView statusView = (TextView) view
+ .findViewById(R.id.account_status);
+ switch (account.getStatus()) {
+ case Account.STATUS_DISABLED:
+ statusView.setText("temporarily disabled");
+ statusView.setTextColor(0xFF1da9da);
+ break;
+ case Account.STATUS_ONLINE:
+ statusView.setText("online");
+ statusView.setTextColor(0xFF83b600);
+ break;
+ case Account.STATUS_OFFLINE:
+ statusView.setText("offline");
+ statusView.setTextColor(0xFFe92727);
+ break;
+ case Account.STATUS_UNAUTHORIZED:
+ statusView.setText("unauthorized");
+ statusView.setTextColor(0xFFe92727);
+ break;
+ case Account.STATUS_SERVER_NOT_FOUND:
+ statusView.setText("server not found");
+ statusView.setTextColor(0xFFe92727);
+ break;
+ default:
+ break;
+ }
+
+ return view;
+ }
+ };
+ final Activity activity = this;
+ accountListView.setAdapter(this.accountListViewAdapter);
+ accountListView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, View view,
+ int position, long arg3) {
+ if (!isActionMode) {
+ EditAccount dialog = new EditAccount();
+ dialog.setAccount(accountList.get(position));
+ dialog.setEditAccountListener(new EditAccountListener() {
+
+ @Override
+ public void onAccountEdited(Account account) {
+ xmppConnectionService.updateAccount(account);
+ }
+ });
+ dialog.show(getFragmentManager(), "edit_account");
+ } else {
+ selectedAccountForActionMode = accountList.get(position);
+ actionMode.invalidate();
+ }
+ }
+ });
+ accountListView.setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> arg0, View view,
+ int position, long arg3) {
+ if (!isActionMode) {
+ accountListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ accountListView.setItemChecked(position,true);
+ selectedAccountForActionMode = accountList.get(position);
+ actionMode = activity.startActionMode((Callback) activity);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ if (xmppConnectionServiceBound) {
+ xmppConnectionService.removeOnAccountListChangedListener();
+ }
+ super.onStop();
+ }
+
+ @Override
+ void onBackendConnected() {
+ xmppConnectionService.setOnAccountListChangedListener(accountChanged);
+ this.accountList.clear();
+ this.accountList.addAll(xmppConnectionService.getAccounts());
+ accountListViewAdapter.notifyDataSetChanged();
+ if (this.accountList.size() == 0) {
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ addAccount();
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.manageaccounts, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ break;
+ case R.id.action_add_account:
+ addAccount();
+ break;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ protected void addAccount() {
+ final Activity activity = this;
+ EditAccount dialog = new EditAccount();
+ dialog.setEditAccountListener(new EditAccountListener() {
+
+ @Override
+ public void onAccountEdited(Account account) {
+ xmppConnectionService.createAccount(account);
+ activity.getActionBar().setDisplayHomeAsUpEnabled(true);
+ }
+ });
+ dialog.show(getFragmentManager(), "add_account");
+ }
+
+ @Override
+ public boolean onActionItemClicked(final ActionMode mode, MenuItem item) {
+ if (item.getItemId()==R.id.account_disable) {
+ selectedAccountForActionMode.setOption(Account.OPTION_DISABLED, true);
+ xmppConnectionService.updateAccount(selectedAccountForActionMode);
+ mode.finish();
+ } else if (item.getItemId()==R.id.account_enable) {
+ selectedAccountForActionMode.setOption(Account.OPTION_DISABLED, false);
+ xmppConnectionService.updateAccount(selectedAccountForActionMode);
+ mode.finish();
+ } else if (item.getItemId()==R.id.account_delete) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Are you sure?");
+ builder.setIconAttribute(android.R.attr.alertDialogIcon);
+ builder.setMessage("If you delete your account your entire conversation history will be lost");
+ builder.setPositiveButton("Delete", new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ xmppConnectionService.deleteAccount(selectedAccountForActionMode);
+ selectedAccountForActionMode = null;
+ mode.finish();
+ }
+ });
+ builder.setNegativeButton("Cancel",null);
+ builder.create().show();
+ } else if (item.getItemId()==R.id.announce_pgp) {
+ mode.finish();
+ try {
+ xmppConnectionService.generatePgpAnnouncement(selectedAccountForActionMode);
+ } catch (PgpEngine.UserInputRequiredException e) {
+ try {
+ startIntentSenderForResult(e.getPendingIntent().getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
+ } catch (SendIntentException e1) {
+ Log.d("gultsch","sending intent failed");
+ }
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ MenuInflater inflater = mode.getMenuInflater();
+ inflater.inflate(R.menu.manageaccounts_context, menu);
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ if (selectedAccountForActionMode.isOptionSet(Account.OPTION_DISABLED)) {
+ menu.findItem(R.id.account_enable).setVisible(true);
+ menu.findItem(R.id.account_disable).setVisible(false);
+ } else {
+ menu.findItem(R.id.account_disable).setVisible(true);
+ menu.findItem(R.id.account_enable).setVisible(false);
+ }
+ return true;
+ }
+
+ @Override
+ public void onActionModeStarted(ActionMode mode) {
+ super.onActionModeStarted(mode);
+ this.isActionMode = true;
+ }
+
+ @Override
+ public void onActionModeFinished(ActionMode mode) {
+ super.onActionModeFinished(mode);
+ this.isActionMode = false;
+ accountListView.clearChoices();
+ accountListView.requestLayout();
+ accountListView.post(new Runnable() {
+ @Override
+ public void run() {
+ accountListView.setChoiceMode(ListView.CHOICE_MODE_NONE);
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK) {
+ if (requestCode == REQUEST_ANNOUNCE_PGP) {
+ try {
+ xmppConnectionService.generatePgpAnnouncement(selectedAccountForActionMode);
+ } catch (UserInputRequiredException e) {
+ Log.d("gultsch","already came back. ignoring");
+ }
+ }
+ }
+ }
+}
diff --git a/src/eu/siacs/conversations/ui/NewConversationActivity.java b/src/eu/siacs/conversations/ui/NewConversationActivity.java
new file mode 100644
index 00000000..628a3047
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/NewConversationActivity.java
@@ -0,0 +1,332 @@
+package eu.siacs.conversations.ui;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.utils.UIHelper;
+import eu.siacs.conversations.utils.Validator;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import android.widget.ImageView;
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+
+public class NewConversationActivity extends XmppActivity {
+
+ protected List<Contact> phoneContacts = new ArrayList<Contact>();
+ protected List<Contact> rosterContacts = new ArrayList<Contact>();
+ protected List<Contact> aggregatedContacts = new ArrayList<Contact>();
+ protected ListView contactsView;
+ protected ArrayAdapter<Contact> contactsAdapter;
+
+ protected EditText search;
+ protected String searchString = "";
+ private TextView contactsHeader;
+ private List<Account> accounts;
+
+ protected void updateAggregatedContacts() {
+
+ aggregatedContacts.clear();
+ for (Contact contact : rosterContacts) {
+ if (contact.match(searchString))
+ aggregatedContacts.add(contact);
+ }
+
+ Collections.sort(aggregatedContacts, new Comparator<Contact>() {
+
+ @SuppressLint("DefaultLocale")
+ @Override
+ public int compare(Contact lhs, Contact rhs) {
+ return lhs.getDisplayName().toLowerCase()
+ .compareTo(rhs.getDisplayName().toLowerCase());
+ }
+ });
+
+ if (aggregatedContacts.size() == 0) {
+
+ if (Validator.isValidJid(searchString)) {
+ String name = searchString.split("@")[0];
+ Contact newContact = new Contact(null, name, searchString, null);
+ newContact.flagAsNotInRoster();
+ aggregatedContacts.add(newContact);
+ contactsHeader.setText("Create new contact");
+ } else {
+ contactsHeader.setText("Contacts");
+ }
+ } else {
+ contactsHeader.setText("Contacts");
+ }
+
+ contactsAdapter.notifyDataSetChanged();
+ contactsView.setScrollX(0);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_new_conversation);
+
+ contactsHeader = (TextView) findViewById(R.id.contacts_header);
+
+ search = (EditText) findViewById(R.id.new_conversation_search);
+ search.addTextChangedListener(new TextWatcher() {
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,
+ int count) {
+ searchString = search.getText().toString();
+ updateAggregatedContacts();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,
+ int after) {
+ // TODO Auto-generated method stub
+
+ }
+ });
+
+ contactsView = (ListView) findViewById(R.id.contactList);
+ contactsAdapter = new ArrayAdapter<Contact>(getApplicationContext(),
+ R.layout.contact, aggregatedContacts) {
+ @Override
+ public View getView(int position, View view, ViewGroup parent) {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ Contact contact = getItem(position);
+ if (view == null) {
+ view = (View) inflater.inflate(R.layout.contact, null);
+ }
+
+ ((TextView) view.findViewById(R.id.contact_display_name))
+ .setText(getItem(position).getDisplayName());
+ TextView contactJid = (TextView) view
+ .findViewById(R.id.contact_jid);
+ contactJid.setText(contact.getJid());
+ String profilePhoto = getItem(position).getProfilePhoto();
+ ImageView imageView = (ImageView) view
+ .findViewById(R.id.contact_photo);
+ if (profilePhoto != null) {
+ imageView.setImageURI(Uri.parse(profilePhoto));
+ } else {
+ imageView.setImageBitmap(UIHelper.getUnknownContactPicture(
+ getItem(position).getDisplayName(), 90));
+ }
+ return view;
+ }
+ };
+ contactsView.setAdapter(contactsAdapter);
+ final Activity activity = this;
+ contactsView.setOnItemClickListener(new OnItemClickListener() {
+
+ @Override
+ public void onItemClick(AdapterView<?> arg0, final View view,
+ int pos, long arg3) {
+ final Contact clickedContact = aggregatedContacts.get(pos);
+
+ if ((clickedContact.getAccount()==null)&&(accounts.size()>1)) {
+ String[] accountList = new String[accounts.size()];
+ for (int i = 0; i < accounts.size(); ++i) {
+ accountList[i] = accounts.get(i).getJid();
+ }
+
+ AlertDialog.Builder accountChooser = new AlertDialog.Builder(
+ activity);
+ accountChooser.setTitle("Choose account");
+ accountChooser.setItems(accountList, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ clickedContact.setAccount(accounts.get(which));
+ showIsMucDialogIfNeeded(clickedContact);
+ }
+ });
+ accountChooser.create().show();
+ } else {
+ if (clickedContact.getAccount()==null) {
+ clickedContact.setAccount(accounts.get(0));
+ }
+ showIsMucDialogIfNeeded(clickedContact);
+ }
+ }
+ });
+ contactsView.setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> arg0, View arg1,
+ int pos, long arg3) {
+ Contact clickedContact = aggregatedContacts.get(pos);
+ DialogContactDetails dialog = new DialogContactDetails();
+ dialog.setContact(clickedContact);
+ dialog.show(getFragmentManager(), "details");
+ return true;
+ }
+ });
+ }
+
+ public void showIsMucDialogIfNeeded(final Contact clickedContact) {
+ if (clickedContact.couldBeMuc()) {
+ AlertDialog.Builder dialog = new AlertDialog.Builder(this);
+ dialog.setTitle("Multi User Conference");
+ dialog.setMessage("Are you trying to join a conference?");
+ dialog.setPositiveButton("Yes", new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startConversation(clickedContact, clickedContact.getAccount(),true);
+ }
+ });
+ dialog.setNegativeButton("No", new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ startConversation(clickedContact, clickedContact.getAccount(),false);
+ }
+ });
+ dialog.create().show();
+ } else {
+ startConversation(clickedContact, clickedContact.getAccount(),false);
+ }
+ }
+
+ public void startConversation(Contact contact, Account account, boolean muc) {
+ if (!contact.isInRoster()) {
+ xmppConnectionService.createContact(contact);
+ }
+ Conversation conversation = xmppConnectionService
+ .findOrCreateConversation(account, contact.getJid(), muc);
+
+ Intent viewConversationIntent = new Intent(this,
+ ConversationActivity.class);
+ viewConversationIntent.setAction(Intent.ACTION_VIEW);
+ viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
+ conversation.getUuid());
+ viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+ viewConversationIntent.setFlags(viewConversationIntent.getFlags()
+ | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(viewConversationIntent);
+ }
+
+ @Override
+ void onBackendConnected() {
+ if (xmppConnectionService.getConversationCount() == 0) {
+ getActionBar().setDisplayHomeAsUpEnabled(false);
+ getActionBar().setHomeButtonEnabled(false);
+ }
+ this.accounts = xmppConnectionService.getAccounts();
+ this.rosterContacts.clear();
+ for (int i = 0; i < accounts.size(); ++i) {
+ xmppConnectionService.getRoster(accounts.get(i),
+ new OnRosterFetchedListener() {
+
+ @Override
+ public void onRosterFetched(List<Contact> roster) {
+ rosterContacts.addAll(roster);
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ updateAggregatedContacts();
+ }
+ });
+
+ }
+ });
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ // Inflate the menu; this adds items to the action bar if it is present.
+ getMenuInflater().inflate(R.menu.newconversation, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_settings:
+ startActivity(new Intent(this, SettingsActivity.class));
+ break;
+ case R.id.action_accounts:
+ startActivity(new Intent(this, ManageAccountActivity.class));
+ break;
+ case R.id.action_refresh_contacts:
+ refreshContacts();
+ break;
+ default:
+ break;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void refreshContacts() {
+ final ProgressBar progress = (ProgressBar) findViewById(R.id.progressBar1);
+ final EditText searchBar = (EditText) findViewById(R.id.new_conversation_search);
+ final TextView contactsHeader = (TextView) findViewById(R.id.contacts_header);
+ final ListView contactList = (ListView) findViewById(R.id.contactList);
+ searchBar.setVisibility(View.GONE);
+ contactsHeader.setVisibility(View.GONE);
+ contactList.setVisibility(View.GONE);
+ progress.setVisibility(View.VISIBLE);
+ this.accounts = xmppConnectionService.getAccounts();
+ this.rosterContacts.clear();
+ for (int i = 0; i < accounts.size(); ++i) {
+ if (accounts.get(i).getStatus() == Account.STATUS_ONLINE) {
+ xmppConnectionService.updateRoster(accounts.get(i),
+ new OnRosterFetchedListener() {
+
+ @Override
+ public void onRosterFetched(
+ final List<Contact> roster) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ rosterContacts.addAll(roster);
+ progress.setVisibility(View.GONE);
+ searchBar.setVisibility(View.VISIBLE);
+ contactList.setVisibility(View.VISIBLE);
+ contactList.setVisibility(View.VISIBLE);
+ updateAggregatedContacts();
+ }
+ });
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/src/eu/siacs/conversations/ui/OnAccountListChangedListener.java b/src/eu/siacs/conversations/ui/OnAccountListChangedListener.java
new file mode 100644
index 00000000..98ef445e
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/OnAccountListChangedListener.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.ui;
+
+public interface OnAccountListChangedListener {
+ public void onAccountListChangedListener();
+}
diff --git a/src/eu/siacs/conversations/ui/OnConversationListChangedListener.java b/src/eu/siacs/conversations/ui/OnConversationListChangedListener.java
new file mode 100644
index 00000000..2a922e21
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/OnConversationListChangedListener.java
@@ -0,0 +1,5 @@
+package eu.siacs.conversations.ui;
+
+public interface OnConversationListChangedListener {
+ public void onConversationListChanged();
+}
diff --git a/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java b/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java
new file mode 100644
index 00000000..d69ce35b
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/OnRosterFetchedListener.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.ui;
+
+import java.util.List;
+
+import eu.siacs.conversations.entities.Contact;
+
+public interface OnRosterFetchedListener {
+ public void onRosterFetched(List<Contact> roster);
+}
diff --git a/src/eu/siacs/conversations/ui/SettingsActivity.java b/src/eu/siacs/conversations/ui/SettingsActivity.java
new file mode 100644
index 00000000..abaf8c68
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/SettingsActivity.java
@@ -0,0 +1,16 @@
+package eu.siacs.conversations.ui;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+public class SettingsActivity extends Activity {
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Display the fragment as the main content.
+ getFragmentManager().beginTransaction()
+ .replace(android.R.id.content, new SettingsFragment()).commit();
+ }
+
+}
diff --git a/src/eu/siacs/conversations/ui/SettingsFragment.java b/src/eu/siacs/conversations/ui/SettingsFragment.java
new file mode 100644
index 00000000..7e1c3698
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/SettingsFragment.java
@@ -0,0 +1,15 @@
+package eu.siacs.conversations.ui;
+
+import eu.siacs.conversations.R;
+import android.os.Bundle;
+import android.preference.PreferenceFragment;
+
+public class SettingsFragment extends PreferenceFragment {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Load the preferences from an XML resource
+ addPreferencesFromResource(R.xml.preferences);
+ }
+}
diff --git a/src/eu/siacs/conversations/ui/XmppActivity.java b/src/eu/siacs/conversations/ui/XmppActivity.java
new file mode 100644
index 00000000..5114e640
--- /dev/null
+++ b/src/eu/siacs/conversations/ui/XmppActivity.java
@@ -0,0 +1,52 @@
+package eu.siacs.conversations.ui;
+
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+
+public abstract class XmppActivity extends Activity {
+ public XmppConnectionService xmppConnectionService;
+ public boolean xmppConnectionServiceBound = false;
+ protected boolean handledViewIntent = false;
+ protected ServiceConnection mConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ XmppConnectionBinder binder = (XmppConnectionBinder) service;
+ xmppConnectionService = binder.getService();
+ xmppConnectionServiceBound = true;
+ onBackendConnected();
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName arg0) {
+ xmppConnectionServiceBound = false;
+ }
+ };
+
+ @Override
+ protected void onStart() {
+ startService(new Intent(this, XmppConnectionService.class));
+ super.onStart();
+ if (!xmppConnectionServiceBound) {
+ Intent intent = new Intent(this, XmppConnectionService.class);
+ bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (xmppConnectionServiceBound) {
+ unbindService(mConnection);
+ xmppConnectionServiceBound = false;
+ }
+ }
+
+ abstract void onBackendConnected();
+}
diff --git a/src/eu/siacs/conversations/utils/DNSHelper.java b/src/eu/siacs/conversations/utils/DNSHelper.java
new file mode 100644
index 00000000..46fd6928
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/DNSHelper.java
@@ -0,0 +1,93 @@
+package eu.siacs.conversations.utils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.util.Random;
+
+import android.os.Bundle;
+import android.util.Log;
+
+public class DNSHelper {
+ public static Bundle getSRVRecord(String host) {
+ Bundle namePort = new Bundle();
+ try {
+ String[] hostParts = host.split("\\.");
+ byte[] transId = new byte[2];
+ Random random = new Random();
+ random.nextBytes(transId);
+ byte[] header = { 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x0c, 0x5f, 0x78, 0x6d, 0x70, 0x70, 0x2d, 0x63,
+ 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x04, 0x5f, 0x74, 0x63, 0x70 };
+ byte[] rest = { 0x00, 0x00, 0x21, 0x00, 0x01, 0x00, 0x00, 0x29,
+ 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ output.write(transId);
+ output.write(header);
+ for (int i = 0; i < hostParts.length; ++i) {
+ char[] tmpChars = hostParts[i].toCharArray();
+ byte[] tmp = new byte[tmpChars.length];
+ for (int j = 0; j < tmpChars.length; ++j) {
+ tmp[j] = (byte) tmpChars[j];
+ }
+ output.write(tmp.length);
+ output.write(tmp);
+ }
+ output.write(rest);
+ byte[] sendPaket = output.toByteArray();
+ byte[] addr = { 0x8, 0x8, 0x8, 0x8 };
+ int realLenght = sendPaket.length - 11;
+ DatagramPacket packet = new DatagramPacket(sendPaket,
+ sendPaket.length, InetAddress.getByAddress(addr), 53);
+ DatagramSocket datagramSocket = new DatagramSocket();
+ datagramSocket.send(packet);
+ byte[] receiveData = new byte[1024];
+
+ DatagramPacket receivePacket = new DatagramPacket(receiveData,
+ receiveData.length);
+ datagramSocket.setSoTimeout(2000);
+ datagramSocket.receive(receivePacket);
+ if (receiveData[3]!=-128) {
+ namePort.putString("error", "nosrv");
+ return namePort;
+ }
+ namePort.putInt("port",calcPort(receiveData[realLenght + 16],
+ receiveData[realLenght + 17]));
+ int i = realLenght + 18;
+ int wordLenght = 0;
+ StringBuilder builder = new StringBuilder();
+ while (receiveData[i] != 0) {
+ if (wordLenght > 0) {
+ builder.append((char) receiveData[i]);
+ --wordLenght;
+ } else {
+ wordLenght = receiveData[i];
+ builder.append(".");
+ }
+ ++i;
+ }
+ builder.replace(0, 1, "");
+ namePort.putString("name",builder.toString());
+ } catch (IOException e) {
+ Log.d("xmppService","gut" + e.getMessage());
+ }
+ return namePort;
+ }
+
+ static int calcPort(byte hb, byte lb) {
+ return ((int) hb << 8) | ((int) lb & 0xFF);
+ }
+
+ final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
+ public static String bytesToHex(byte[] bytes) {
+ char[] hexChars = new char[bytes.length * 2];
+ for ( int j = 0; j < bytes.length; j++ ) {
+ int v = bytes[j] & 0xFF;
+ hexChars[j * 2] = hexArray[v >>> 4];
+ hexChars[j * 2 + 1] = hexArray[v & 0x0F];
+ }
+ return new String(hexChars);
+ }
+}
diff --git a/src/eu/siacs/conversations/utils/MessageParser.java b/src/eu/siacs/conversations/utils/MessageParser.java
new file mode 100644
index 00000000..dc0cd35c
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/MessageParser.java
@@ -0,0 +1,149 @@
+package eu.siacs.conversations.utils;
+
+import java.util.List;
+
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+import android.util.Log;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.services.XmppConnectionService;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xmpp.MessagePacket;
+
+public class MessageParser {
+
+ protected static final String LOGTAG = "xmppService";
+
+ public static Message parsePlainTextChat(MessagePacket packet, Account account, XmppConnectionService service) {
+ String[] fromParts = packet.getFrom().split("/");
+ Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false);
+ String body = packet.getBody();
+ return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_NONE, Message.STATUS_RECIEVED);
+ }
+
+ public static Message parsePgpChat(String pgpBody, MessagePacket packet, Account account, XmppConnectionService service) {
+ String[] fromParts = packet.getFrom().split("/");
+ Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false);
+ return new Message(conversation, packet.getFrom(), pgpBody, Message.ENCRYPTION_PGP, Message.STATUS_RECIEVED);
+ }
+
+ public static Message parseOtrChat(MessagePacket packet, Account account, XmppConnectionService service) {
+ String[] fromParts = packet.getFrom().split("/");
+ Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false);
+ String body = packet.getBody();
+ if (!conversation.hasValidOtrSession()) {
+ conversation.startOtrSession(service.getApplicationContext(), fromParts[1]);
+ }
+ try {
+ Session otrSession = conversation.getOtrSession();
+ SessionStatus before = otrSession
+ .getSessionStatus();
+ body = otrSession.transformReceiving(body);
+ SessionStatus after = otrSession.getSessionStatus();
+ if ((before != after)
+ && (after == SessionStatus.ENCRYPTED)) {
+ Log.d(LOGTAG, "otr session etablished");
+ List<Message> messages = conversation
+ .getMessages();
+ for (int i = 0; i < messages.size(); ++i) {
+ Message msg = messages.get(i);
+ if ((msg.getStatus() == Message.STATUS_UNSEND)
+ && (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
+ MessagePacket outPacket = service.prepareMessagePacket(
+ account, msg, otrSession);
+ msg.setStatus(Message.STATUS_SEND);
+ service.databaseBackend.updateMessage(msg);
+ account.getXmppConnection()
+ .sendMessagePacket(outPacket);
+ }
+ }
+ if (service.convChangedListener!=null) {
+ service.convChangedListener.onConversationListChanged();
+ }
+ } else if ((before != after) && (after == SessionStatus.FINISHED)) {
+ conversation.resetOtrSession();
+ Log.d(LOGTAG,"otr session stoped");
+ }
+ } catch (Exception e) {
+ Log.d(LOGTAG, "error receiving otr. resetting");
+ conversation.resetOtrSession();
+ return null;
+ }
+ if (body == null) {
+ return null;
+ }
+ return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_OTR,Message.STATUS_RECIEVED);
+ }
+
+ public static Message parseGroupchat(MessagePacket packet, Account account, XmppConnectionService service) {
+ int status;
+ String[] fromParts = packet.getFrom().split("/");
+ Conversation conversation = service.findOrCreateConversation(account, fromParts[0],true);
+ if ((fromParts.length == 1) || (packet.hasChild("subject"))) {
+ return null;
+ }
+ String counterPart = fromParts[1];
+ if (counterPart.equals(account.getUsername())) {
+ status = Message.STATUS_SEND;
+ } else {
+ status = Message.STATUS_RECIEVED;
+ }
+ return new Message(conversation, counterPart, packet.getBody(), Message.ENCRYPTION_NONE, status);
+ }
+
+ public static Message parseCarbonMessage(MessagePacket packet,
+ Account account, XmppConnectionService service) {
+ // TODO Auto-generated method stub
+ int status;
+ String fullJid;
+ Element forwarded;
+ if (packet.hasChild("received")) {
+ forwarded = packet.findChild("received").findChild(
+ "forwarded");
+ status = Message.STATUS_RECIEVED;
+ } else if (packet.hasChild("sent")) {
+ forwarded = packet.findChild("sent").findChild(
+ "forwarded");
+ status = Message.STATUS_SEND;
+ } else {
+ return null;
+ }
+ Element message = forwarded.findChild("message");
+ if ((message == null) || (!message.hasChild("body")))
+ return null; // either malformed or boring
+ if (status == Message.STATUS_RECIEVED) {
+ fullJid = message.getAttribute("from");
+ } else {
+ fullJid = message.getAttribute("to");
+ }
+ String[] parts = fullJid.split("/");
+ Conversation conversation = service.findOrCreateConversation(account, parts[0],false);
+ return new Message(conversation,fullJid, message.findChild("body").getContent(), Message.ENCRYPTION_NONE,status);
+ }
+
+ public static Message parseError(MessagePacket packet, Account account, XmppConnectionService service) {
+
+ String[] fromParts = packet.getFrom().split("/");
+ Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false);
+ Element error = packet.findChild("error");
+ String errorName = error.getChildren().get(0).getName();
+ String displayError;
+ if (errorName.equals("service-unavailable")) {
+ displayError = "Contact is offline and does not have offline storage";
+ } else {
+ displayError = errorName.replace("-", " ");
+ }
+ return new Message(conversation, packet.getFrom(), displayError, Message.ENCRYPTION_NONE, Message.STATUS_ERROR);
+ }
+
+ public static String getPgpBody(MessagePacket packet) {
+ for(Element child : packet.getChildren()) {
+ if (child.getName().equals("x")&&child.getAttribute("xmlns").equals("jabber:x:encrypted")) {
+ return child.getContent();
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java b/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
new file mode 100644
index 00000000..fa8cea04
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/OnPhoneContactsLoadedListener.java
@@ -0,0 +1,9 @@
+package eu.siacs.conversations.utils;
+
+import java.util.Hashtable;
+
+import android.os.Bundle;
+
+public interface OnPhoneContactsLoadedListener {
+ public void onPhoneContactsLoaded(Hashtable<String, Bundle> phoneContacts);
+}
diff --git a/src/eu/siacs/conversations/utils/PhoneHelper.java b/src/eu/siacs/conversations/utils/PhoneHelper.java
new file mode 100644
index 00000000..e28f817e
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/PhoneHelper.java
@@ -0,0 +1,87 @@
+package eu.siacs.conversations.utils;
+
+import java.util.Hashtable;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.CursorLoader;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Looper;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.Profile;
+
+public class PhoneHelper {
+
+ public static void loadPhoneContacts(Context context, final OnPhoneContactsLoadedListener listener) {
+ if (Looper.myLooper()==null) {
+ Looper.prepare();
+ }
+ final Looper mLooper = Looper.myLooper();
+ final Hashtable<String, Bundle> phoneContacts = new Hashtable<String, Bundle>();
+
+ final String[] PROJECTION = new String[] {
+ ContactsContract.Data._ID,
+ ContactsContract.Data.DISPLAY_NAME,
+ ContactsContract.Data.PHOTO_THUMBNAIL_URI,
+ ContactsContract.Data.LOOKUP_KEY,
+ ContactsContract.CommonDataKinds.Im.DATA };
+
+ final String SELECTION = "(" + ContactsContract.Data.MIMETYPE + "=\""
+ + ContactsContract.CommonDataKinds.Im.CONTENT_ITEM_TYPE
+ + "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
+ + "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
+ + "\")";
+
+ CursorLoader mCursorLoader = new CursorLoader(context,
+ ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null,
+ null);
+ mCursorLoader.registerListener(0, new OnLoadCompleteListener<Cursor>() {
+
+ @Override
+ public void onLoadComplete(Loader<Cursor> arg0, Cursor cursor) {
+ while (cursor.moveToNext()) {
+ Bundle contact = new Bundle();
+ contact.putInt("phoneid", cursor.getInt(cursor
+ .getColumnIndex(ContactsContract.Data._ID)));
+ contact.putString(
+ "displayname",
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.DISPLAY_NAME)));
+ contact.putString(
+ "photouri",
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.PHOTO_THUMBNAIL_URI)));
+ contact.putString("lookup",cursor.getString(cursor
+ .getColumnIndex(ContactsContract.Data.LOOKUP_KEY)));
+ phoneContacts.put(
+ cursor.getString(cursor
+ .getColumnIndex(ContactsContract.CommonDataKinds.Im.DATA)),
+ contact);
+ }
+ if (listener!=null) {
+ listener.onPhoneContactsLoaded(phoneContacts);
+ }
+ mLooper.quit();
+ }
+ });
+ mCursorLoader.startLoading();
+ }
+
+ public static Uri getSefliUri(Activity activity) {
+ String[] mProjection = new String[] { Profile._ID,
+ Profile.PHOTO_THUMBNAIL_URI };
+ Cursor mProfileCursor = activity.getContentResolver().query(
+ Profile.CONTENT_URI, mProjection, null, null, null);
+
+ if (mProfileCursor.getCount()==0) {
+ return null;
+ } else {
+ mProfileCursor.moveToFirst();
+ return Uri.parse(mProfileCursor.getString(1));
+ }
+ }
+}
diff --git a/src/eu/siacs/conversations/utils/SASL.java b/src/eu/siacs/conversations/utils/SASL.java
new file mode 100644
index 00000000..cda1f97b
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/SASL.java
@@ -0,0 +1,24 @@
+package eu.siacs.conversations.utils;
+
+import android.util.Base64;
+
+public class SASL {
+ public static String plain(String username, String password) {
+ byte[] userBytes = username.getBytes();
+ int userLenght = userBytes.length;
+ byte[] passwordBytes = password.getBytes();
+ byte[] saslBytes = new byte[userBytes.length+passwordBytes.length+2];
+ saslBytes[0] = 0x0;
+ for(int i = 1; i < saslBytes.length; ++i) {
+ if (i<=userLenght) {
+ saslBytes[i] = userBytes[i-1];
+ } else if (i==userLenght+1) {
+ saslBytes[i] = 0x0;
+ } else {
+ saslBytes[i] = passwordBytes[i-(userLenght+2)];
+ }
+ }
+
+ return Base64.encodeToString(saslBytes, Base64.DEFAULT);
+ }
+}
diff --git a/src/eu/siacs/conversations/utils/UIHelper.java b/src/eu/siacs/conversations/utils/UIHelper.java
new file mode 100644
index 00000000..f79fe14c
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/UIHelper.java
@@ -0,0 +1,210 @@
+package eu.siacs.conversations.utils;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.entities.Contact;
+import eu.siacs.conversations.entities.Conversation;
+import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.ConversationActivity;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.provider.ContactsContract.Contacts;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.TaskStackBuilder;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+public class UIHelper {
+ public static String readableTimeDifference(long time) {
+ if (time == 0) {
+ return "just now";
+ }
+ Date date = new Date(time);
+ long difference = (System.currentTimeMillis() - time) / 1000;
+ if (difference < 60) {
+ return "just now";
+ } else if (difference < 60 * 10) {
+ return difference / 60 + " min ago";
+ } else if (difference < 60 * 60 * 24) {
+ SimpleDateFormat sdf = new SimpleDateFormat("HH:mm");
+ return sdf.format(date);
+ } else {
+ SimpleDateFormat sdf = new SimpleDateFormat("MM/dd");
+ return sdf.format(date);
+ }
+ }
+
+ public static Bitmap getUnknownContactPicture(String name, int size) {
+ String firstLetter = name.substring(0, 1).toUpperCase();
+
+ int holoColors[] = { 0xFF1da9da, 0xFFb368d9, 0xFF83b600, 0xFFffa713,
+ 0xFFe92727 };
+
+ int color = holoColors[Math.abs(name.hashCode()) % holoColors.length];
+
+ Bitmap bitmap = Bitmap
+ .createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ bitmap.eraseColor(color);
+
+ Paint paint = new Paint();
+ paint.setColor(0xffe5e5e5);
+ paint.setTextSize((float) (size * 0.9));
+ paint.setAntiAlias(true);
+ Rect rect = new Rect();
+ paint.getTextBounds(firstLetter, 0, 1, rect);
+ float width = paint.measureText(firstLetter);
+ canvas.drawText(firstLetter, (size / 2) - (width / 2), (size / 2)
+ + (rect.height() / 2), paint);
+
+ return bitmap;
+ }
+
+ public static Bitmap getErrorPicture(int size) {
+ Bitmap bitmap = Bitmap
+ .createBitmap(size, size, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ bitmap.eraseColor(0xFFe92727);
+
+ Paint paint = new Paint();
+ paint.setColor(0xffe5e5e5);
+ paint.setTextSize((float) (size * 0.9));
+ paint.setAntiAlias(true);
+ Rect rect = new Rect();
+ paint.getTextBounds("!", 0, 1, rect);
+ float width = paint.measureText("!");
+ canvas.drawText("!", (size / 2) - (width / 2), (size / 2)
+ + (rect.height() / 2), paint);
+
+ return bitmap;
+ }
+
+ public static Notification getUnreadMessageNotification(Context context,
+ Conversation conversation) {
+
+ SharedPreferences sharedPref = PreferenceManager
+ .getDefaultSharedPreferences(context);
+ String ringtone = sharedPref.getString("notification_ringtone", null);
+
+ Resources res = context.getResources();
+ NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
+ context);
+ mBuilder.setLargeIcon(UIHelper.getUnknownContactPicture(conversation
+ .getName(), (int) res
+ .getDimension(android.R.dimen.notification_large_icon_width)));
+ mBuilder.setContentTitle(conversation.getName());
+ mBuilder.setTicker(conversation.getLatestMessage().getBody().trim());
+ StringBuilder bigText = new StringBuilder();
+ List<Message> messages = conversation.getMessages();
+ String firstLine = "";
+ for(int i = messages.size() -1; i >= 0; --i) {
+ if (!messages.get(i).isRead()) {
+ if (i == messages.size() -1 ) {
+ firstLine = messages.get(i).getBody().trim();
+ bigText.append(firstLine);
+ } else {
+ firstLine = messages.get(i).getBody().trim();
+ bigText.insert(0, firstLine+"\n");
+ }
+ } else {
+ break;
+ }
+ }
+ mBuilder.setContentText(firstLine);
+ mBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText.toString()));
+ mBuilder.setSmallIcon(R.drawable.notification);
+ mBuilder.setLights(0xffffffff, 2000, 4000);
+ if (ringtone != null) {
+ mBuilder.setSound(Uri.parse(ringtone));
+ }
+
+ TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
+ stackBuilder.addParentStack(ConversationActivity.class);
+
+ Intent viewConversationIntent = new Intent(context,
+ ConversationActivity.class);
+ viewConversationIntent.setAction(Intent.ACTION_VIEW);
+ viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
+ conversation.getUuid());
+ viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
+
+ stackBuilder.addNextIntent(viewConversationIntent);
+
+ PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ mBuilder.setContentIntent(resultPendingIntent);
+ return mBuilder.build();
+ }
+
+ public static void prepareContactBadge(final Activity activity,
+ QuickContactBadge badge, final Contact contact) {
+ if (contact.getSystemAccount()!=null) {
+ String[] systemAccount = contact.getSystemAccount().split("#");
+ long id = Long.parseLong(systemAccount[0]);
+ badge.assignContactUri(Contacts.getLookupUri(id, systemAccount[1]));
+
+ if (contact.getProfilePhoto() != null) {
+ badge.setImageURI(Uri.parse(contact.getProfilePhoto()));
+ } else {
+ badge.setImageBitmap(UIHelper.getUnknownContactPicture(contact.getDisplayName(), 400));
+ }
+ } else {
+ badge.setImageBitmap(UIHelper.getUnknownContactPicture(contact.getDisplayName(), 400));
+ }
+
+ }
+
+ public static AlertDialog getVerifyFingerprintDialog(final ConversationActivity activity,final Conversation conversation, final LinearLayout msg) {
+ final Contact contact = conversation.getContact();
+ final Account account = conversation.getAccount();
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle("Verify fingerprint");
+ LayoutInflater inflater = activity.getLayoutInflater();
+ View view = inflater.inflate(R.layout.dialog_verify_otr, null);
+ TextView jid = (TextView) view.findViewById(R.id.verify_otr_jid);
+ TextView fingerprint = (TextView) view.findViewById(R.id.verify_otr_fingerprint);
+ TextView yourprint = (TextView) view.findViewById(R.id.verify_otr_yourprint);
+
+ jid.setText(contact.getJid());
+ fingerprint.setText(conversation.getOtrFingerprint());
+ yourprint.setText(account.getOtrFingerprint());
+ builder.setNegativeButton("Cancel", null);
+ builder.setPositiveButton("Verify", new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ contact.addOtrFingerprint(conversation.getOtrFingerprint());
+ msg.setVisibility(View.GONE);
+ activity.xmppConnectionService.updateContact(contact);
+ }
+ });
+ builder.setView(view);
+ return builder.create();
+ }
+}
diff --git a/src/eu/siacs/conversations/utils/Validator.java b/src/eu/siacs/conversations/utils/Validator.java
new file mode 100644
index 00000000..fce953ae
--- /dev/null
+++ b/src/eu/siacs/conversations/utils/Validator.java
@@ -0,0 +1,14 @@
+package eu.siacs.conversations.utils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Validator {
+ public static final Pattern VALID_JID =
+ Pattern.compile("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$", Pattern.CASE_INSENSITIVE);
+
+ public static boolean isValidJid(String jid) {
+ Matcher matcher = VALID_JID.matcher(jid);
+ return matcher.find();
+ }
+}
diff --git a/src/eu/siacs/conversations/xml/Element.java b/src/eu/siacs/conversations/xml/Element.java
new file mode 100644
index 00000000..ad95ef9c
--- /dev/null
+++ b/src/eu/siacs/conversations/xml/Element.java
@@ -0,0 +1,101 @@
+package eu.siacs.conversations.xml;
+
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+import android.util.Log;
+
+public class Element {
+ protected String name;
+ protected Hashtable<String, String> attributes = new Hashtable<String, String>();
+ protected String content;
+ protected List<Element> children = new ArrayList<Element>();
+
+ public Element(String name) {
+ this.name = name;
+ }
+
+ public Element addChild(Element child) {
+ this.content = null;
+ children.add(child);
+ return this;
+ }
+
+ public Element setContent(String content) {
+ this.content = content;
+ this.children.clear();
+ return this;
+ }
+
+ public Element findChild(String name) {
+ for(Element child : this.children) {
+ if (child.getName().equals(name)) {
+ return child;
+ }
+ }
+ return null;
+ }
+
+ public boolean hasChild(String name) {
+ for(Element child : this.children) {
+ if (child.getName().equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public List<Element> getChildren() {
+ return this.children;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public Element setAttribute(String name, String value) {
+ this.attributes.put(name, value);
+ return this;
+ }
+
+ public Element setAttributes(Hashtable<String, String> attributes) {
+ this.attributes = attributes;
+ return this;
+ }
+
+ public String getAttribute(String name) {
+ if (this.attributes.containsKey(name)) {
+ return this.attributes.get(name);
+ } else {
+ return null;
+ }
+ }
+
+ public String toString() {
+ StringBuilder elementOutput = new StringBuilder();
+ if ((content==null)&&(children.size() == 0)) {
+ Tag emptyTag = Tag.empty(name);
+ emptyTag.setAtttributes(this.attributes);
+ elementOutput.append(emptyTag.toString());
+ } else {
+ Tag startTag = Tag.start(name);
+ startTag.setAtttributes(this.attributes);
+ elementOutput.append(startTag);
+ if (content!=null) {
+ elementOutput.append(content);
+ } else {
+ for(Element child : children) {
+ elementOutput.append(child.toString());
+ }
+ }
+ Tag endTag = Tag.end(name);
+ elementOutput.append(endTag);
+ }
+ return elementOutput.toString();
+ }
+
+ public String getName() {
+ return name;
+ }
+}
diff --git a/src/eu/siacs/conversations/xml/Tag.java b/src/eu/siacs/conversations/xml/Tag.java
new file mode 100644
index 00000000..970cf0ae
--- /dev/null
+++ b/src/eu/siacs/conversations/xml/Tag.java
@@ -0,0 +1,99 @@
+package eu.siacs.conversations.xml;
+
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class Tag {
+ public static final int NO = -1;
+ public static final int START = 0;
+ public static final int END = 1;
+ public static final int EMPTY = 2;
+
+ protected int type;
+ protected String name;
+ protected Hashtable<String, String> attributes = new Hashtable<String, String>();
+
+ protected Tag(int type, String name) {
+ this.type = type;
+ this.name = name;
+ }
+
+
+ public static Tag no(String text) {
+ return new Tag(NO,text);
+ }
+
+ public static Tag start(String name) {
+ return new Tag(START,name);
+ }
+
+ public static Tag end(String name) {
+ return new Tag(END,name);
+ }
+
+ public static Tag empty(String name) {
+ return new Tag(EMPTY,name);
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getAttribute(String attrName) {
+ return this.attributes.get(attrName);
+ }
+
+ public Tag setAttribute(String attrName, String attrValue) {
+ this.attributes.put(attrName, attrValue);
+ return this;
+ }
+
+ public Tag setAtttributes(Hashtable<String, String> attributes) {
+ this.attributes = attributes;
+ return this;
+ }
+
+ public boolean isStart(String needle) {
+ return (this.type == START) && (this.name.equals(needle));
+ }
+
+ public boolean isEnd(String needle) {
+ return (this.type == END) && (this.name.equals(needle));
+ }
+
+ public boolean isNo() {
+ return (this.type == NO);
+ }
+
+ public String toString() {
+ StringBuilder tagOutput = new StringBuilder();
+ tagOutput.append('<');
+ if (type==END) {
+ tagOutput.append('/');
+ }
+ tagOutput.append(name);
+ if(type!=END) {
+ Set<Entry<String, String>> attributeSet = attributes.entrySet();
+ Iterator<Entry<String, String>> it = attributeSet.iterator();
+ while(it.hasNext()) {
+ Entry<String,String> entry = it.next();
+ tagOutput.append(' ');
+ tagOutput.append(entry.getKey());
+ tagOutput.append("=\"");
+ tagOutput.append(entry.getValue());
+ tagOutput.append('"');
+ }
+ }
+ if (type==EMPTY) {
+ tagOutput.append('/');
+ }
+ tagOutput.append('>');
+ return tagOutput.toString();
+ }
+
+ public Hashtable<String, String> getAttributes() {
+ return this.attributes;
+ }
+}
diff --git a/src/eu/siacs/conversations/xml/TagWriter.java b/src/eu/siacs/conversations/xml/TagWriter.java
new file mode 100644
index 00000000..109078ca
--- /dev/null
+++ b/src/eu/siacs/conversations/xml/TagWriter.java
@@ -0,0 +1,64 @@
+package eu.siacs.conversations.xml;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import android.util.Log;
+
+public class TagWriter {
+
+ private OutputStreamWriter outputStream;
+ private LinkedBlockingQueue<String> writeQueue = new LinkedBlockingQueue<String>();
+ private Thread writer = new Thread() {
+ public boolean shouldStop = false;
+ @Override
+ public void run() {
+ while(!shouldStop) {
+ try {
+ String output = writeQueue.take();
+ outputStream.write(output);
+ outputStream.flush();
+ } catch (IOException e) {
+ Log.d("xmppService", "error writing to stream");
+ } catch (InterruptedException e) {
+
+ }
+ }
+ }
+ };
+
+
+ public TagWriter() {
+
+ }
+
+ public TagWriter(OutputStream out) {
+ this.setOutputStream(out);
+ writer.start();
+ }
+
+ public void setOutputStream(OutputStream out) {
+ this.outputStream = new OutputStreamWriter(out);
+ if (!writer.isAlive()) writer.start();
+ }
+
+ public TagWriter beginDocument() {
+ writeQueue.add("<?xml version='1.0'?>");
+ return this;
+ }
+
+ public TagWriter writeTag(Tag tag) {
+ writeQueue.add(tag.toString());
+ return this;
+ }
+
+ public void writeString(String string) {
+ writeQueue.add(string);
+ }
+
+ public void writeElement(Element element) {
+ writeQueue.add(element.toString());
+ }
+}
diff --git a/src/eu/siacs/conversations/xml/XmlReader.java b/src/eu/siacs/conversations/xml/XmlReader.java
new file mode 100644
index 00000000..131141dd
--- /dev/null
+++ b/src/eu/siacs/conversations/xml/XmlReader.java
@@ -0,0 +1,102 @@
+package eu.siacs.conversations.xml;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.util.Log;
+import android.util.Xml;
+
+public class XmlReader {
+ private static final String LOGTAG = "xmppService";
+ private XmlPullParser parser;
+ private PowerManager.WakeLock wakeLock;
+ private InputStream is;
+
+ public XmlReader(WakeLock wakeLock) {
+ this.parser = Xml.newPullParser();
+ try {
+ this.parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES,true);
+ } catch (XmlPullParserException e) {
+ Log.d(LOGTAG,"error setting namespace feature on parser");
+ }
+ this.wakeLock = wakeLock;
+ }
+
+ public void setInputStream(InputStream inputStream) {
+ this.is = inputStream;
+ try {
+ parser.setInput(new InputStreamReader(this.is));
+ } catch (XmlPullParserException e) {
+ Log.d(LOGTAG,"error setting input stream");
+ }
+ }
+
+ public void reset() {
+ try {
+ parser.setInput(new InputStreamReader(this.is));
+ } catch (XmlPullParserException e) {
+ Log.d(LOGTAG,"error resetting input stream");
+ }
+ }
+
+ public Tag readTag() throws XmlPullParserException, IOException {
+ if (wakeLock.isHeld()) {
+ //Log.d(LOGTAG,"there was a wake lock. releasing it till next event");
+ wakeLock.release(); //release wake look while waiting on next parser event
+ }
+ //Log.d(LOGTAG,"waiting for new event...");
+ while(parser.next() != XmlPullParser.END_DOCUMENT) {
+ //Log.d(LOGTAG,"found new event. acquiring wake lock");
+ wakeLock.acquire();
+ if (parser.getEventType() == XmlPullParser.START_TAG) {
+ Tag tag = Tag.start(parser.getName());
+ for(int i = 0; i < parser.getAttributeCount(); ++i) {
+ tag.setAttribute(parser.getAttributeName(i), parser.getAttributeValue(i));
+ }
+ String xmlns = parser.getNamespace();
+ if (xmlns!=null) {
+ tag.setAttribute("xmlns",xmlns);
+ }
+ return tag;
+ } else if (parser.getEventType() == XmlPullParser.END_TAG) {
+ Tag tag = Tag.end(parser.getName());
+ return tag;
+ } else if (parser.getEventType() == XmlPullParser.TEXT) {
+ Tag tag = Tag.no(parser.getText());
+ return tag;
+ }
+ }
+ if (wakeLock.isHeld()) {
+ wakeLock.release();
+ }
+ return null; //end document;
+ }
+
+ public Element readElement(Tag currentTag) throws XmlPullParserException, IOException {
+ Element element = new Element(currentTag.getName());
+ //Log.d(LOGTAG,"trying to read element "+element.getName());
+ element.setAttributes(currentTag.getAttributes());
+ Tag nextTag = this.readTag();
+ //Log.d(LOGTAG,"next Tag is: "+nextTag.toString());
+ if(nextTag.isNo()) {
+ element.setContent(nextTag.getName());
+ nextTag = this.readTag();
+ }
+ //Log.d(LOGTAG,"reading till the end of "+element.getName());
+ while(!nextTag.isEnd(element.getName())) {
+ if (!nextTag.isNo()) {
+ Element child = this.readElement(nextTag);
+ element.addChild(child);
+ }
+ nextTag = this.readTag();
+ }
+ //Log.d(LOGTAG,"return with element"+element);
+ return element;
+ }
+}
diff --git a/src/eu/siacs/conversations/xmpp/IqPacket.java b/src/eu/siacs/conversations/xmpp/IqPacket.java
new file mode 100644
index 00000000..2319fd28
--- /dev/null
+++ b/src/eu/siacs/conversations/xmpp/IqPacket.java
@@ -0,0 +1,40 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.xml.Element;
+
+public class IqPacket extends Element {
+
+ public static final int TYPE_SET = 0;
+ public static final int TYPE_RESULT = 1;
+ public static final int TYPE_GET = 2;
+
+ private IqPacket(String name) {
+ super(name);
+ }
+
+ public IqPacket(int type) {
+ super("iq");
+ switch (type) {
+ case TYPE_SET:
+ this.setAttribute("type", "set");
+ break;
+ case TYPE_GET:
+ this.setAttribute("type", "get");
+ break;
+ case TYPE_RESULT:
+ this.setAttribute("type", "result");
+ break;
+ default:
+ break;
+ }
+ }
+
+ public IqPacket() {
+ super("iq");
+ }
+
+ public String getId() {
+ return this.getAttribute("id");
+ }
+
+}
diff --git a/src/eu/siacs/conversations/xmpp/MessagePacket.java b/src/eu/siacs/conversations/xmpp/MessagePacket.java
new file mode 100644
index 00000000..a014155f
--- /dev/null
+++ b/src/eu/siacs/conversations/xmpp/MessagePacket.java
@@ -0,0 +1,81 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.xml.Element;
+
+public class MessagePacket extends Element {
+ public static final int TYPE_CHAT = 0;
+ public static final int TYPE_UNKNOWN = 1;
+ public static final int TYPE_NO = 2;
+ public static final int TYPE_GROUPCHAT = 3;
+ public static final int TYPE_ERROR = 4;
+
+ private MessagePacket(String name) {
+ super(name);
+ }
+
+ public MessagePacket() {
+ super("message");
+ }
+
+ public String getTo() {
+ return getAttribute("to");
+ }
+
+ public String getFrom() {
+ return getAttribute("from");
+ }
+
+ public String getBody() {
+ Element body = this.findChild("body");
+ if (body!=null) {
+ return body.getContent();
+ } else {
+ return null;
+ }
+ }
+
+ public void setTo(String to) {
+ setAttribute("to", to);
+ }
+
+ public void setFrom(String from) {
+ setAttribute("from",from);
+ }
+
+ public void setBody(String text) {
+ this.children.remove(findChild("body"));
+ Element body = new Element("body");
+ body.setContent(text);
+ this.children.add(body);
+ }
+
+ public void setType(int type) {
+ switch (type) {
+ case TYPE_CHAT:
+ this.setAttribute("type","chat");
+ break;
+ case TYPE_GROUPCHAT:
+ this.setAttribute("type", "groupchat");
+ break;
+ default:
+ this.setAttribute("type","chat");
+ break;
+ }
+ }
+
+ public int getType() {
+ String type = getAttribute("type");
+ if (type==null) {
+ return TYPE_NO;
+ }
+ if (type.equals("chat")) {
+ return TYPE_CHAT;
+ } else if (type.equals("groupchat")) {
+ return TYPE_GROUPCHAT;
+ } else if (type.equals("error")) {
+ return TYPE_ERROR;
+ } else {
+ return TYPE_UNKNOWN;
+ }
+ }
+}
diff --git a/src/eu/siacs/conversations/xmpp/OnIqPacketReceived.java b/src/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
new file mode 100644
index 00000000..4e09282c
--- /dev/null
+++ b/src/eu/siacs/conversations/xmpp/OnIqPacketReceived.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnIqPacketReceived {
+ public void onIqPacketReceived(Account account, IqPacket packet);
+}
diff --git a/src/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java b/src/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
new file mode 100644
index 00000000..3d169300
--- /dev/null
+++ b/src/eu/siacs/conversations/xmpp/OnMessagePacketReceived.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnMessagePacketReceived {
+ public void onMessagePacketReceived(Account account, MessagePacket packet);
+}
diff --git a/src/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java b/src/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
new file mode 100644
index 00000000..058d8be9
--- /dev/null
+++ b/src/eu/siacs/conversations/xmpp/OnPresencePacketReceived.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnPresencePacketReceived {
+ public void onPresencePacketReceived(Account account, PresencePacket packet);
+}
diff --git a/src/eu/siacs/conversations/xmpp/OnStatusChanged.java b/src/eu/siacs/conversations/xmpp/OnStatusChanged.java
new file mode 100644
index 00000000..ad1d98cb
--- /dev/null
+++ b/src/eu/siacs/conversations/xmpp/OnStatusChanged.java
@@ -0,0 +1,7 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.entities.Account;
+
+public interface OnStatusChanged {
+ public void onStatusChanged(Account account);
+}
diff --git a/src/eu/siacs/conversations/xmpp/PresencePacket.java b/src/eu/siacs/conversations/xmpp/PresencePacket.java
new file mode 100644
index 00000000..3d77ce15
--- /dev/null
+++ b/src/eu/siacs/conversations/xmpp/PresencePacket.java
@@ -0,0 +1,13 @@
+package eu.siacs.conversations.xmpp;
+
+import eu.siacs.conversations.xml.Element;
+
+public class PresencePacket extends Element {
+ private PresencePacket(String name) {
+ super("presence");
+ }
+
+ public PresencePacket() {
+ super("presence");
+ }
+}
diff --git a/src/eu/siacs/conversations/xmpp/XmppConnection.java b/src/eu/siacs/conversations/xmpp/XmppConnection.java
new file mode 100644
index 00000000..4583c145
--- /dev/null
+++ b/src/eu/siacs/conversations/xmpp/XmppConnection.java
@@ -0,0 +1,446 @@
+package eu.siacs.conversations.xmpp;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigInteger;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.HashSet;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import android.os.Bundle;
+import android.os.PowerManager;
+import android.util.Log;
+import eu.siacs.conversations.entities.Account;
+import eu.siacs.conversations.utils.DNSHelper;
+import eu.siacs.conversations.utils.SASL;
+import eu.siacs.conversations.xml.Element;
+import eu.siacs.conversations.xml.Tag;
+import eu.siacs.conversations.xml.TagWriter;
+import eu.siacs.conversations.xml.XmlReader;
+
+public class XmppConnection implements Runnable {
+
+ protected Account account;
+ private static final String LOGTAG = "xmppService";
+
+ private PowerManager.WakeLock wakeLock;
+
+ private SecureRandom random = new SecureRandom();
+
+ private Socket socket;
+ private XmlReader tagReader;
+ private TagWriter tagWriter;
+
+ private boolean isTlsEncrypted = false;
+ private boolean isAuthenticated = false;
+ // private boolean shouldUseTLS = false;
+ private boolean shouldConnect = true;
+ private boolean shouldBind = true;
+ private boolean shouldAuthenticate = true;
+ private Element streamFeatures;
+ private HashSet<String> discoFeatures = new HashSet<String>();
+
+ private static final int PACKET_IQ = 0;
+ private static final int PACKET_MESSAGE = 1;
+ private static final int PACKET_PRESENCE = 2;
+
+ private Hashtable<String, OnIqPacketReceived> iqPacketCallbacks = new Hashtable<String, OnIqPacketReceived>();
+ private OnPresencePacketReceived presenceListener = null;
+ private OnIqPacketReceived unregisteredIqListener = null;
+ private OnMessagePacketReceived messageListener = null;
+ private OnStatusChanged statusListener = null;
+
+ public XmppConnection(Account account, PowerManager pm) {
+ this.account = account;
+ wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ "XmppConnection");
+ tagReader = new XmlReader(wakeLock);
+ tagWriter = new TagWriter();
+ }
+
+ protected void connect() {
+ try {
+ Bundle namePort = DNSHelper.getSRVRecord(account.getServer());
+ String srvRecordServer = namePort.getString("name");
+ int srvRecordPort = namePort.getInt("port");
+ if (srvRecordServer != null) {
+ Log.d(LOGTAG, account.getJid() + ": using values from dns "
+ + srvRecordServer + ":" + srvRecordPort);
+ socket = new Socket(srvRecordServer, srvRecordPort);
+ } else {
+ socket = new Socket(account.getServer(), 5222);
+ }
+ OutputStream out = socket.getOutputStream();
+ tagWriter.setOutputStream(out);
+ InputStream in = socket.getInputStream();
+ tagReader.setInputStream(in);
+ tagWriter.beginDocument();
+ sendStartStream();
+ Tag nextTag;
+ while ((nextTag = tagReader.readTag()) != null) {
+ if (nextTag.isStart("stream")) {
+ processStream(nextTag);
+ break;
+ } else {
+ Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName());
+ return;
+ }
+ }
+ if (socket.isConnected()) {
+ socket.close();
+ }
+ } catch (UnknownHostException e) {
+ account.setStatus(Account.STATUS_SERVER_NOT_FOUND);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ return;
+ } catch (IOException e) {
+ Log.d(LOGTAG, "bla " + e.getMessage());
+ if (shouldConnect) {
+ Log.d(LOGTAG, account.getJid() + ": connection lost");
+ account.setStatus(Account.STATUS_OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ } catch (XmlPullParserException e) {
+ Log.d(LOGTAG, "xml exception " + e.getMessage());
+ return;
+ }
+
+ }
+
+ @Override
+ public void run() {
+ shouldConnect = true;
+ while (shouldConnect) {
+ connect();
+ try {
+ if (shouldConnect) {
+ Thread.sleep(30000);
+ }
+ } catch (InterruptedException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+ Log.d(LOGTAG, "end run");
+ }
+
+ private void processStream(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ Tag nextTag = tagReader.readTag();
+ while ((nextTag != null) && (!nextTag.isEnd("stream"))) {
+ if (nextTag.isStart("error")) {
+ processStreamError(nextTag);
+ } else if (nextTag.isStart("features")) {
+ processStreamFeatures(nextTag);
+ } else if (nextTag.isStart("proceed")) {
+ switchOverToTls(nextTag);
+ } else if (nextTag.isStart("success")) {
+ isAuthenticated = true;
+ Log.d(LOGTAG, account.getJid()
+ + ": read success tag in stream. reset again");
+ tagReader.readTag();
+ tagReader.reset();
+ sendStartStream();
+ processStream(tagReader.readTag());
+ break;
+ } else if (nextTag.isStart("failure")) {
+ Element failure = tagReader.readElement(nextTag);
+ Log.d(LOGTAG, "read failure element" + failure.toString());
+ account.setStatus(Account.STATUS_UNAUTHORIZED);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ tagWriter.writeTag(Tag.end("stream"));
+ } else if (nextTag.isStart("iq")) {
+ processIq(nextTag);
+ } else if (nextTag.isStart("message")) {
+ processMessage(nextTag);
+ } else if (nextTag.isStart("presence")) {
+ processPresence(nextTag);
+ } else {
+ Log.d(LOGTAG, "found unexpected tag: " + nextTag.getName()
+ + " as child of " + currentTag.getName());
+ }
+ nextTag = tagReader.readTag();
+ }
+ if (account.getStatus() == Account.STATUS_ONLINE) {
+ account.setStatus(Account.STATUS_OFFLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ }
+ }
+
+ private Element processPacket(Tag currentTag, int packetType)
+ throws XmlPullParserException, IOException {
+ Element element;
+ switch (packetType) {
+ case PACKET_IQ:
+ element = new IqPacket();
+ break;
+ case PACKET_MESSAGE:
+ element = new MessagePacket();
+ break;
+ case PACKET_PRESENCE:
+ element = new PresencePacket();
+ break;
+ default:
+ return null;
+ }
+ element.setAttributes(currentTag.getAttributes());
+ Tag nextTag = tagReader.readTag();
+ while (!nextTag.isEnd(element.getName())) {
+ if (!nextTag.isNo()) {
+ Element child = tagReader.readElement(nextTag);
+ element.addChild(child);
+ }
+ nextTag = tagReader.readTag();
+ }
+ return element;
+ }
+
+ private IqPacket processIq(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ IqPacket packet = (IqPacket) processPacket(currentTag, PACKET_IQ);
+ if (iqPacketCallbacks.containsKey(packet.getId())) {
+ iqPacketCallbacks.get(packet.getId()).onIqPacketReceived(account,
+ packet);
+ iqPacketCallbacks.remove(packet.getId());
+ } else if (this.unregisteredIqListener != null) {
+ this.unregisteredIqListener.onIqPacketReceived(account, packet);
+ }
+ return packet;
+ }
+
+ private void processMessage(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ MessagePacket packet = (MessagePacket) processPacket(currentTag,
+ PACKET_MESSAGE);
+ if (this.messageListener != null) {
+ this.messageListener.onMessagePacketReceived(account, packet);
+ }
+ }
+
+ private void processPresence(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ PresencePacket packet = (PresencePacket) processPacket(currentTag,
+ PACKET_PRESENCE);
+ if (this.presenceListener != null) {
+ this.presenceListener.onPresencePacketReceived(account, packet);
+ }
+ }
+
+ private void sendStartTLS() {
+ Tag startTLS = Tag.empty("starttls");
+ startTLS.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-tls");
+ Log.d(LOGTAG, account.getJid() + ": sending starttls");
+ tagWriter.writeTag(startTLS);
+ }
+
+ private void switchOverToTls(Tag currentTag) throws XmlPullParserException,
+ IOException {
+ Tag nextTag = tagReader.readTag(); // should be proceed end tag
+ Log.d(LOGTAG, account.getJid() + ": now switch to ssl");
+ SSLSocket sslSocket;
+ try {
+ sslSocket = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory
+ .getDefault()).createSocket(socket, socket.getInetAddress()
+ .getHostAddress(), socket.getPort(), true);
+ tagReader.setInputStream(sslSocket.getInputStream());
+ Log.d(LOGTAG, "reset inputstream");
+ tagWriter.setOutputStream(sslSocket.getOutputStream());
+ Log.d(LOGTAG, "switch over seemed to work");
+ isTlsEncrypted = true;
+ sendStartStream();
+ processStream(tagReader.readTag());
+ sslSocket.close();
+ } catch (IOException e) {
+ Log.d(LOGTAG,
+ account.getJid() + ": error on ssl '" + e.getMessage()
+ + "'");
+ }
+ }
+
+ private void sendSaslAuth() throws IOException, XmlPullParserException {
+ String saslString = SASL.plain(account.getUsername(),
+ account.getPassword());
+ Element auth = new Element("auth");
+ auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl");
+ auth.setAttribute("mechanism", "PLAIN");
+ auth.setContent(saslString);
+ Log.d(LOGTAG, account.getJid() + ": sending sasl " + auth.toString());
+ tagWriter.writeElement(auth);
+ }
+
+ private void processStreamFeatures(Tag currentTag)
+ throws XmlPullParserException, IOException {
+ this.streamFeatures = tagReader.readElement(currentTag);
+ Log.d(LOGTAG, account.getJid() + ": process stream features "
+ + streamFeatures);
+ if (this.streamFeatures.hasChild("starttls")
+ && account.isOptionSet(Account.OPTION_USETLS)) {
+ sendStartTLS();
+ } else if (this.streamFeatures.hasChild("mechanisms")
+ && shouldAuthenticate) {
+ sendSaslAuth();
+ }
+ if (this.streamFeatures.hasChild("bind") && shouldBind) {
+ sendBindRequest();
+ if (this.streamFeatures.hasChild("session")) {
+ IqPacket startSession = new IqPacket(IqPacket.TYPE_SET);
+ Element session = new Element("session");
+ session.setAttribute("xmlns",
+ "urn:ietf:params:xml:ns:xmpp-session");
+ session.setContent("");
+ startSession.addChild(session);
+ sendIqPacket(startSession, null);
+ tagWriter.writeElement(startSession);
+ }
+ Element presence = new Element("presence");
+
+ tagWriter.writeElement(presence);
+ }
+ }
+
+ private void sendBindRequest() throws IOException {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ Element bind = new Element("bind");
+ bind.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-bind");
+ iq.addChild(bind);
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ String resource = packet.findChild("bind").findChild("jid")
+ .getContent().split("/")[1];
+ account.setResource(resource);
+ account.setStatus(Account.STATUS_ONLINE);
+ if (statusListener != null) {
+ statusListener.onStatusChanged(account);
+ }
+ sendServiceDiscovery();
+ }
+ });
+ }
+
+ private void sendServiceDiscovery() {
+ IqPacket iq = new IqPacket(IqPacket.TYPE_GET);
+ iq.setAttribute("to", account.getServer());
+ Element query = new Element("query");
+ query.setAttribute("xmlns", "http://jabber.org/protocol/disco#info");
+ iq.addChild(query);
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (packet.hasChild("query")) {
+ List<Element> elements = packet.findChild("query")
+ .getChildren();
+ for (int i = 0; i < elements.size(); ++i) {
+ if (elements.get(i).getName().equals("feature")) {
+ discoFeatures.add(elements.get(i).getAttribute(
+ "var"));
+ }
+ }
+ }
+ if (discoFeatures.contains("urn:xmpp:carbons:2")) {
+ sendEnableCarbons();
+ }
+ }
+ });
+ }
+
+ private void sendEnableCarbons() {
+ Log.d(LOGTAG,account.getJid()+": enable carbons");
+ IqPacket iq = new IqPacket(IqPacket.TYPE_SET);
+ Element enable = new Element("enable");
+ enable.setAttribute("xmlns", "urn:xmpp:carbons:2");
+ iq.addChild(enable);
+ this.sendIqPacket(iq, new OnIqPacketReceived() {
+
+ @Override
+ public void onIqPacketReceived(Account account, IqPacket packet) {
+ if (!packet.hasChild("error")) {
+ Log.d(LOGTAG,account.getJid()+": successfully enabled carbons");
+ } else {
+ Log.d(LOGTAG,account.getJid()+": error enableing carbons "+packet.toString());
+ }
+ }
+ });
+ }
+
+ private void processStreamError(Tag currentTag) {
+ Log.d(LOGTAG, "processStreamError");
+ }
+
+ private void sendStartStream() {
+ Tag stream = Tag.start("stream:stream");
+ stream.setAttribute("from", account.getJid());
+ stream.setAttribute("to", account.getServer());
+ stream.setAttribute("version", "1.0");
+ stream.setAttribute("xml:lang", "en");
+ stream.setAttribute("xmlns", "jabber:client");
+ stream.setAttribute("xmlns:stream", "http://etherx.jabber.org/streams");
+ tagWriter.writeTag(stream);
+ }
+
+ private String nextRandomId() {
+ return new BigInteger(50, random).toString(32);
+ }
+
+ public void sendIqPacket(IqPacket packet, OnIqPacketReceived callback) {
+ String id = nextRandomId();
+ packet.setAttribute("id", id);
+ tagWriter.writeElement(packet);
+ if (callback != null) {
+ iqPacketCallbacks.put(id, callback);
+ }
+ //Log.d(LOGTAG, account.getJid() + ": sending: " + packet.toString());
+ }
+
+ public void sendMessagePacket(MessagePacket packet) {
+ Log.d(LOGTAG,"sending message packet "+packet.toString());
+ tagWriter.writeElement(packet);
+ }
+
+ public void sendPresencePacket(PresencePacket packet) {
+ tagWriter.writeElement(packet);
+ Log.d(LOGTAG, account.getJid() + ": sending: " + packet.toString());
+ }
+
+ public void setOnMessagePacketReceivedListener(
+ OnMessagePacketReceived listener) {
+ this.messageListener = listener;
+ }
+
+ public void setOnUnregisteredIqPacketReceivedListener(
+ OnIqPacketReceived listener) {
+ this.unregisteredIqListener = listener;
+ }
+
+ public void setOnPresencePacketReceivedListener(
+ OnPresencePacketReceived listener) {
+ this.presenceListener = listener;
+ }
+
+ public void setOnStatusChangedListener(OnStatusChanged listener) {
+ this.statusListener = listener;
+ }
+
+ public void disconnect() {
+ shouldConnect = false;
+ tagWriter.writeTag(Tag.end("stream:stream"));
+ }
+}