basic otr support

Daniel Gultsch created

Change summary

gen/de/gultsch/chat/R.java                              |  17 
libs/bcprov-jdk15on-150.jar                             |   0 
libs/otr4j-0.10.jar                                     |   0 
res/drawable-hdpi/ic_action_secure.png                  |   0 
res/drawable-mdpi/ic_action_secure.png                  |   0 
res/drawable-xhdpi/ic_action_secure.png                 |   0 
res/drawable-xxhdpi/ic_action_secure.png                |   0 
src/de/gultsch/chat/crypto/OtrEngine.java               | 229 ++++++++
src/de/gultsch/chat/entities/Account.java               |  53 +
src/de/gultsch/chat/entities/Conversation.java          |  54 +
src/de/gultsch/chat/persistance/DatabaseBackend.java    |   2 
src/de/gultsch/chat/services/XmppConnectionService.java | 305 ++++++++--
src/de/gultsch/chat/ui/ConversationActivity.java        |  18 
src/de/gultsch/chat/ui/ConversationFragment.java        |  35 
src/de/gultsch/chat/utils/UIHelper.java                 |   2 
src/de/gultsch/chat/xml/TagWriter.java                  |   5 
src/de/gultsch/chat/xmpp/MessagePacket.java             |   1 
src/de/gultsch/chat/xmpp/XmppConnection.java            |   3 
18 files changed, 611 insertions(+), 113 deletions(-)

Detailed changes

gen/de/gultsch/chat/R.java 🔗

@@ -31,14 +31,15 @@ public final class R {
         public static final int ic_action_add_person=0x7f020002;
         public static final int ic_action_delete=0x7f020003;
         public static final int ic_action_refresh=0x7f020004;
-        public static final int ic_action_send=0x7f020005;
-        public static final int ic_action_send_now=0x7f020006;
-        public static final int ic_action_unsecure=0x7f020007;
-        public static final int ic_launcher=0x7f020008;
-        public static final int ic_profile=0x7f020009;
-        public static final int message_border=0x7f02000a;
-        public static final int notification=0x7f02000b;
-        public static final int section_header=0x7f02000c;
+        public static final int ic_action_secure=0x7f020005;
+        public static final int ic_action_send=0x7f020006;
+        public static final int ic_action_send_now=0x7f020007;
+        public static final int ic_action_unsecure=0x7f020008;
+        public static final int ic_launcher=0x7f020009;
+        public static final int ic_profile=0x7f02000a;
+        public static final int message_border=0x7f02000b;
+        public static final int notification=0x7f02000c;
+        public static final int section_header=0x7f02000d;
     }
     public static final class id {
         public static final int account_confirm_password_desc=0x7f0a0019;

src/de/gultsch/chat/crypto/OtrEngine.java 🔗

@@ -0,0 +1,229 @@
+package de.gultsch.chat.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 de.gultsch.chat.entities.Account;
+import de.gultsch.chat.persistance.DatabaseBackend;
+import de.gultsch.chat.xml.Element;
+import de.gultsch.chat.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();
+		} catch (JSONException 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;
+	}
+
+	@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);
+		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
+
+	}
+
+}

src/de/gultsch/chat/entities/Account.java 🔗

@@ -1,7 +1,14 @@
 package de.gultsch.chat.entities;
 
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import de.gultsch.chat.crypto.OtrEngine;
+import de.gultsch.chat.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{
@@ -15,6 +22,7 @@ public class Account  extends AbstractEntity{
 	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;
@@ -34,23 +42,32 @@ public class Account  extends AbstractEntity{
 	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;
+	
 	public Account() {
 		this.uuid = "0";
 	}
 	
 	public Account(String username, String server, String password) {
-		this(java.util.UUID.randomUUID().toString(),username,server,password,0,null);
+		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) {
+	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) {
@@ -108,6 +125,14 @@ public class Account  extends AbstractEntity{
 	public String getJid() {
 		return username+"@"+server;
 	}
+	
+	public JSONObject getKeys() {
+		return keys;
+	}
+	
+	public void setKey(String keyName, String keyValue) throws JSONException {
+		this.keys.put(keyName, keyValue);
+	}
 
 	@Override
 	public ContentValues getContentValues() {
@@ -117,6 +142,8 @@ public class Account  extends AbstractEntity{
 		values.put(SERVER, server);
 		values.put(PASSWORD, password);
 		values.put(OPTIONS,options);
+		values.put(KEYS,this.keys.toString());
+		values.put(ROSTERVERSION,rosterVersion);
 		return values;
 	}
 	
@@ -126,8 +153,28 @@ public class Account  extends AbstractEntity{
 				cursor.getString(cursor.getColumnIndex(SERVER)),
 				cursor.getString(cursor.getColumnIndex(PASSWORD)),
 				cursor.getInt(cursor.getColumnIndex(OPTIONS)),
-				cursor.getString(cursor.getColumnIndex(ROSTERVERSION))
+				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;
+	}
 }

src/de/gultsch/chat/entities/Conversation.java 🔗

@@ -3,7 +3,16 @@ package de.gultsch.chat.entities;
 import java.util.ArrayList;
 import java.util.List;
 
+import de.gultsch.chat.crypto.OtrEngine;
+import de.gultsch.chat.xmpp.XmppConnection;
+
+import net.java.otr4j.OtrException;
+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;
@@ -40,6 +49,9 @@ public class Conversation extends AbstractEntity {
 	private transient List<Message> messages = null;
 	private transient Account account = null;
 	private transient Contact contact;
+	
+	private transient SessionImpl otrSession;
+	private transient String foreignOtrPresence;
 
 	public Conversation(String name, Account account,
 			String contactJid, int mode) {
@@ -85,19 +97,13 @@ public class Conversation extends AbstractEntity {
 		}
 	}
 	
-	public String getLatestMessage() {
-		if ((this.messages == null)||(this.messages.size()==0)) {
-			return null;
-		} else {
-			return this.messages.get(this.messages.size() - 1).getBody();
-		}
-	}
-	
-	public long getLatestMessageDate() {
+	public Message getLatestMessage() {
 		if ((this.messages == null)||(this.messages.size()==0)) {
-			return this.getCreated();
+			Message message = new Message(this,"",Message.ENCRYPTION_NONE);
+			message.setTime(0);
+			return message;
 		} else {
-			return this.messages.get(this.messages.size() - 1).getTimeSent();
+			return this.messages.get(this.messages.size() - 1);
 		}
 	}
 
@@ -198,4 +204,30 @@ public class Conversation extends AbstractEntity {
 	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));
+	}
+	
+	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();
+			}
+		}
+	}
+
+	public boolean hasOtrSession() {
+		return (this.otrSession!=null);
+	}
 }

src/de/gultsch/chat/persistance/DatabaseBackend.java 🔗

@@ -35,7 +35,7 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 				+ " TEXT PRIMARY KEY," + Account.USERNAME + " TEXT,"
 				+ Account.SERVER + " TEXT," + Account.PASSWORD + " TEXT,"
 				+ Account.ROSTERVERSION + " TEXT," + Account.OPTIONS
-				+ " NUMBER)");
+				+ " NUMBER, "+Account.KEYS+" TEXT)");
 		db.execSQL("create table " + Conversation.TABLENAME + " ("
 				+ Conversation.UUID + " TEXT PRIMARY KEY, " + Conversation.NAME
 				+ " TEXT, " + Conversation.CONTACT + " TEXT, "

src/de/gultsch/chat/services/XmppConnectionService.java 🔗

@@ -6,6 +6,12 @@ import java.util.ArrayList;
 import java.util.Date;
 import java.util.Hashtable;
 import java.util.List;
+import java.util.Set;
+
+import net.java.otr4j.OtrException;
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionImpl;
+import net.java.otr4j.session.SessionStatus;
 
 import de.gultsch.chat.entities.Account;
 import de.gultsch.chat.entities.Contact;
@@ -50,8 +56,6 @@ public class XmppConnectionService extends Service {
 	private List<Account> accounts;
 	private List<Conversation> conversations = null;
 
-	private Hashtable<Account, XmppConnection> connections = new Hashtable<Account, XmppConnection>();
-
 	private OnConversationListChangedListener convChangedListener = null;
 	private OnAccountListChangedListener accountChangedListener = null;
 
@@ -73,7 +77,9 @@ public class XmppConnectionService extends Service {
 			if ((packet.getType() == MessagePacket.TYPE_CHAT)
 					|| (packet.getType() == MessagePacket.TYPE_GROUPCHAT)) {
 				boolean notify = true;
+				boolean runOtrCheck = false;
 				int status = Message.STATUS_RECIEVED;
+				int encryption = Message.ENCRYPTION_NONE;
 				String body;
 				String fullJid;
 				if (!packet.hasChild("body")) {
@@ -106,6 +112,7 @@ public class XmppConnectionService extends Service {
 				} else {
 					fullJid = packet.getFrom();
 					body = packet.getBody();
+					runOtrCheck = true;
 				}
 				Conversation conversation = null;
 				String[] fromParts = fullJid.split("/");
@@ -124,9 +131,51 @@ public class XmppConnectionService extends Service {
 					}
 				} else {
 					counterPart = fullJid;
+					if ((runOtrCheck) && body.startsWith("?OTR")) {
+						if (!conversation.hasOtrSession()) {
+							conversation.startOtrSession(
+									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 = prepareMessagePacket(
+												account, msg, otrSession);
+										msg.setStatus(Message.STATUS_SEND);
+										databaseBackend.updateMessage(msg);
+										account.getXmppConnection()
+												.sendMessagePacket(outPacket);
+									}
+								}
+								if (convChangedListener!=null) {
+									convChangedListener.onConversationListChanged();
+								}
+							}
+						} catch (Exception e) {
+							Log.d(LOGTAG, "error receiving otr. resetting");
+							conversation.resetOtrSession();
+							return;
+						}
+						if (body == null) {
+							return;
+						}
+						encryption = Message.ENCRYPTION_OTR;
+					}
 				}
 				Message message = new Message(conversation, counterPart, body,
-						Message.ENCRYPTION_NONE, status);
+						encryption, status);
 				if (packet.hasChild("delay")) {
 					try {
 						String stamp = packet.findChild("delay").getAttribute(
@@ -169,14 +218,14 @@ public class XmppConnectionService extends Service {
 				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();
- 				}
+				for (int i = 0; i < conversations.size(); ++i) {
+					if (conversations.get(i).getAccount() == account) {
+						sendUnsendMessages(conversations.get(i));
+					}
+				}
+				if (convChangedListener != null) {
+					convChangedListener.onConversationListChanged();
+				}
 			}
 		}
 	};
@@ -216,8 +265,19 @@ public class XmppConnectionService extends Service {
 					databaseBackend.updateContact(contact);
 				}
 			}
+			replaceContactInConversation(contact);
 		}
 	};
+	
+	private void replaceContactInConversation(Contact contact) {
+		List<Conversation> conversations = getConversations();
+		for(int i = 0; i < conversations.size(); ++i) {
+			if (conversations.get(i).getContact().equals(contact)) {
+				conversations.get(i).setContact(contact);
+				break;
+			}
+		}
+	}
 
 	public class XmppConnectionBinder extends Binder {
 		public XmppConnectionService getService() {
@@ -228,13 +288,9 @@ public class XmppConnectionService extends Service {
 	@Override
 	public int onStartCommand(Intent intent, int flags, int startId) {
 		for (Account account : accounts) {
-			if (!connections.containsKey(account)) {
+			if (account.getXmppConnection() == null) {
 				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
-					this.connections.put(account,
-							this.createConnection(account));
-				} else {
-					Log.d(LOGTAG, account.getJid()
-							+ ": not starting because it's disabled");
+					account.setXmppConnection(this.createConnection(account));
 				}
 			}
 		}
@@ -250,6 +306,16 @@ public class XmppConnectionService extends Service {
 				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
 	}
 
+	@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);
@@ -261,40 +327,105 @@ public class XmppConnectionService extends Service {
 		return connection;
 	}
 
+	private void startOtrSession(Conversation conv) {
+		Set<String> presences = conv.getContact().getPresences()
+				.keySet();
+		if (presences.size() == 0) {
+			Log.d(LOGTAG, "counter part isnt online. cant use otr");
+			return;
+		} else if (presences.size() == 1) {
+			conv.startOtrSession(getApplicationContext(),
+					(String) presences.toArray()[0]);
+			try {
+				conv.getOtrSession().startSession();
+			} catch (OtrException e) {
+				Log.d(LOGTAG, "couldnt actually start");
+			}
+		} else {
+			String latestCounterpartPresence = null;
+			List<Message> messages = conv.getMessages();
+			for (int i = messages.size() - 1; i >= 0; --i) {
+				if (messages.get(i).getStatus() == Message.STATUS_RECIEVED) {
+					String[] parts = messages.get(i).getCounterpart()
+							.split("/");
+					if (parts.length == 2) {
+						latestCounterpartPresence = parts[1];
+						break;
+					}
+				}
+			}
+			if (presences.contains(latestCounterpartPresence)) {
+				conv.startOtrSession(getApplicationContext(),
+						latestCounterpartPresence);
+				try {
+					conv.getOtrSession().startSession();
+				} catch (OtrException e) {
+					// TODO Auto-generated catch block
+					Log.d(LOGTAG, "couldnt actually start");
+				}
+			} else {
+				Log.d(LOGTAG,
+						"could not decide where to send otr connection to");
+			}
+		}
+	}
+	
 	public void sendMessage(Account account, Message message) {
-
+		Conversation conv = message.getConversation();
+		boolean saveInDb = false;
+		boolean addToConversation = false;
 		if (account.getStatus() == Account.STATUS_ONLINE) {
-			MessagePacket packet = prepareMessagePacket(account, message);
-			connections.get(account).sendMessagePacket(packet);
-			if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
-				message.setStatus(Message.STATUS_SEND);
+			MessagePacket packet;
+			if (message.getEncryption() == Message.ENCRYPTION_OTR) {
+				if (!conv.hasOtrSession()) {
+					//starting otr session. messages will be send later
+					startOtrSession(conv);
+				} else {
+					//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 {
+				// don't encrypt
 				if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
-					databaseBackend.createMessage(message);
-					message.getConversation().getMessages().add(message);
-					if (convChangedListener!=null) {
-						convChangedListener.onConversationListChanged();
-					}
+					message.setStatus(Message.STATUS_SEND);
+					saveInDb = true;
+					addToConversation = true;
 				}
+				
+				packet = prepareMessagePacket(account, message, null);
+				account.getXmppConnection().sendMessagePacket(packet);
 			}
 		} else {
-			message.getConversation().getMessages().add(message);
+			// account is offline
+			saveInDb = true;
+			addToConversation = true;
+
+		}
+		if (saveInDb) {
 			databaseBackend.createMessage(message);
-			if (convChangedListener!=null) {
+		}
+		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);
+				Message message = conversation.getMessages().get(i);
 				MessagePacket packet = prepareMessagePacket(
-						conversation.getAccount(),message);
-				connections.get(conversation.getAccount()).sendMessagePacket(
-						packet);
+						conversation.getAccount(), message, null);
+				conversation.getAccount().getXmppConnection()
+						.sendMessagePacket(packet);
 				message.setStatus(Message.STATUS_SEND);
 				if (conversation.getMode() == Conversation.MODE_SINGLE) {
 					databaseBackend.updateMessage(message);
@@ -307,16 +438,37 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
-	private MessagePacket prepareMessagePacket(Account account, Message message) {
+	private 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());
 		}
-		packet.setTo(message.getCounterpart());
-		packet.setFrom(account.getJid());
-		packet.setBody(message.getBody());
 		return packet;
 	}
 
@@ -345,7 +497,7 @@ public class XmppConnectionService extends Service {
 						query.setAttribute("xmlns", "jabber:iq:roster");
 						query.setAttribute("ver", "");
 						iqPacket.addChild(query);
-						connections.get(account).sendIqPacket(iqPacket,
+						account.getXmppConnection().sendIqPacket(iqPacket,
 								new OnIqPacketReceived() {
 
 									@Override
@@ -488,7 +640,7 @@ public class XmppConnectionService extends Service {
 			if (muc) {
 				conversation.setMode(Conversation.MODE_MULTI);
 				if (account.getStatus() == Account.STATUS_ONLINE) {
-					joinMuc(account, conversation);
+					joinMuc(conversation);
 				}
 			} else {
 				conversation.setMode(Conversation.MODE_SINGLE);
@@ -506,7 +658,7 @@ public class XmppConnectionService extends Service {
 				conversation = new Conversation(conversationName, account, jid,
 						Conversation.MODE_MULTI);
 				if (account.getStatus() == Account.STATUS_ONLINE) {
-					joinMuc(account, conversation);
+					joinMuc(conversation);
 				}
 			} else {
 				conversation = new Conversation(conversationName, account, jid,
@@ -523,6 +675,17 @@ public class XmppConnectionService extends Service {
 	}
 
 	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) {
@@ -537,23 +700,18 @@ public class XmppConnectionService extends Service {
 	public void createAccount(Account account) {
 		databaseBackend.createAccount(account);
 		this.accounts.add(account);
-		this.connections.put(account, this.createConnection(account));
+		account.setXmppConnection(this.createConnection(account));
 		if (accountChangedListener != null)
 			accountChangedListener.onAccountListChangedListener();
 	}
 
 	public void updateAccount(Account account) {
 		databaseBackend.updateAccount(account);
-		XmppConnection connection = this.connections.get(account);
-		if (connection != null) {
-			connection.disconnect();
-			this.connections.remove(account);
+		if (account.getXmppConnection() != null) {
+			disconnect(account);
 		}
 		if (!account.isOptionSet(Account.OPTION_DISABLED)) {
-			this.connections.put(account, this.createConnection(account));
-		} else {
-			Log.d(LOGTAG, account.getJid()
-					+ ": not starting because it's disabled");
+			account.setXmppConnection(this.createConnection(account));
 		}
 		if (accountChangedListener != null)
 			accountChangedListener.onAccountListChangedListener();
@@ -561,10 +719,8 @@ public class XmppConnectionService extends Service {
 
 	public void deleteAccount(Account account) {
 		Log.d(LOGTAG, "called delete account");
-		if (this.connections.containsKey(account)) {
-			Log.d(LOGTAG, "found connection. disconnecting");
-			this.connections.get(account).disconnect();
-			this.connections.remove(account);
+		if (account.getXmppConnection() != null) {
+			this.disconnect(account);
 		}
 		databaseBackend.deleteAccount(account);
 		this.accounts.remove(account);
@@ -596,31 +752,54 @@ public class XmppConnectionService extends Service {
 			Conversation conversation = conversations.get(i);
 			if ((conversation.getMode() == Conversation.MODE_MULTI)
 					&& (conversation.getAccount() == account)) {
-				joinMuc(account, conversation);
+				joinMuc(conversation);
 			}
 		}
 	}
 
-	public void joinMuc(Account account, Conversation conversation) {
+	public void joinMuc(Conversation conversation) {
 		String muc = conversation.getContactJid();
 		PresencePacket packet = new PresencePacket();
-		packet.setAttribute("to", muc + "/" + account.getUsername());
+		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");
-			history.setAttribute(
-					"seconds",
+			history.setAttribute("seconds",
 					(System.currentTimeMillis() - conversation
-							.getLatestMessageDate()) / 1000 + "");
+							.getLatestMessage().getTimeSent() / 1000) + "");
 			x.addChild(history);
 		}
 		packet.addChild(x);
-		connections.get(conversation.getAccount()).sendPresencePacket(packet);
+		conversation.getAccount().getXmppConnection()
+				.sendPresencePacket(packet);
 	}
 
-	public void disconnectMultiModeConversations() {
+	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

src/de/gultsch/chat/ui/ConversationActivity.java 🔗

@@ -9,6 +9,7 @@ import de.gultsch.chat.R;
 import de.gultsch.chat.R.id;
 import de.gultsch.chat.entities.Contact;
 import de.gultsch.chat.entities.Conversation;
+import de.gultsch.chat.entities.Message;
 import de.gultsch.chat.utils.UIHelper;
 import android.net.Uri;
 import android.os.Bundle;
@@ -114,7 +115,7 @@ public class ConversationActivity extends XmppActivity {
 			Collections.sort(this.conversationList, new Comparator<Conversation>() {
 				@Override
 				public int compare(Conversation lhs, Conversation rhs) {
-					return (int) (rhs.getLatestMessageDate() - lhs.getLatestMessageDate());
+					return (int) (rhs.getLatestMessage().getTimeSent() - lhs.getLatestMessage().getTimeSent());
 				}
 			});
 		}
@@ -143,7 +144,7 @@ public class ConversationActivity extends XmppActivity {
 				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());
+				convLastMsg.setText(conv.getLatestMessage().getBody());
 				
 				if(!conv.isRead()) {
 					convName.setTypeface(null,Typeface.BOLD);
@@ -154,7 +155,7 @@ public class ConversationActivity extends XmppActivity {
 				}
 				
 				((TextView) view.findViewById(R.id.conversation_lastupdate))
-				.setText(UIHelper.readableTimeDifference(getItem(position).getLatestMessageDate()));
+				.setText(UIHelper.readableTimeDifference(getItem(position).getLatestMessage().getTimeSent()));
 				
 				Uri profilePhoto = getItem(position).getProfilePhotoUri();
 				ImageView imageView = (ImageView) view.findViewById(R.id.conversation_image);
@@ -238,18 +239,23 @@ public class ConversationActivity extends XmppActivity {
 	@Override
 	public boolean onCreateOptionsMenu(Menu menu) {
 		getMenuInflater().inflate(R.menu.conversations, menu);
-
+		MenuItem menuSecure = (MenuItem) menu.findItem(R.id.action_security);
+		
 		if (spl.isOpen()) {
 			((MenuItem) menu.findItem(R.id.action_archive)).setVisible(false);
 			((MenuItem) menu.findItem(R.id.action_details)).setVisible(false);
-			((MenuItem) menu.findItem(R.id.action_security)).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) {
 					((MenuItem) menu.findItem(R.id.action_security)).setVisible(false);
-					((MenuItem) menu.findItem(R.id.action_details)).setVisible(false);
+					menuSecure.setVisible(false);
 					((MenuItem) menu.findItem(R.id.action_archive)).setTitle("Leave conference");
+				} else {
+					if (this.getSelectedConversation().getLatestMessage().getEncryption() != Message.ENCRYPTION_NONE) {
+						menuSecure.setIcon(R.drawable.ic_action_secure);
+					}
 				}
 			}
 		}

src/de/gultsch/chat/ui/ConversationFragment.java 🔗

@@ -15,7 +15,6 @@ import android.graphics.Typeface;
 import android.net.Uri;
 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;
@@ -36,38 +35,30 @@ public class ConversationFragment extends Fragment {
 	protected ArrayAdapter<Message> messageListAdapter;
 	protected Contact contact;
 	
+	private EditText chatMsg;
+	private int nextMessageEncryption = Message.ENCRYPTION_NONE;
+	
 	@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) view.findViewById(R.id.textSendButton))
 				.setOnClickListener(new OnClickListener() {
 
 					@Override
 					public void onClick(View v) {
 						ConversationActivity activity = (ConversationActivity) getActivity();
-						EditText chatMsg = (EditText) view
-								.findViewById(R.id.textinput);
 						if (chatMsg.getText().length() < 1)
 							return;
 						Message message = new Message(conversation, chatMsg
-								.getText().toString(), Message.ENCRYPTION_NONE);
+								.getText().toString(), nextMessageEncryption);
 						activity.xmppConnectionService.sendMessage(conversation.getAccount(),message);
 						chatMsg.setText("");
-						
-						/*if (conversation.getMode()==Conversation.MODE_SINGLE) {
-							conversation.getMessages().add(message);
-							messageList.add(message);
-						}*/
-						
-						//activity.updateConversationList();
-						
-						//messagesView.setSelection(messageList.size() - 1);
 					}
 				});
 
@@ -213,6 +204,22 @@ public class ConversationFragment extends Fragment {
 		this.messageList.clear();
 		this.messageList.addAll(this.conversation.getMessages());
 		this.messageListAdapter.notifyDataSetChanged();
+		if (messageList.size()>=1) {
+			nextMessageEncryption = this.conversation.getLatestMessage().getEncryption();
+		}
+		getActivity().invalidateOptionsMenu();
+		switch (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");
+		default:
+			break;
+		}
 		int size = this.messageList.size();
 		if (size >= 1)
 			messagesView.setSelection(size - 1);

src/de/gultsch/chat/utils/UIHelper.java 🔗

@@ -92,7 +92,7 @@ public class UIHelper {
 				.getName(), (int) res
 				.getDimension(android.R.dimen.notification_large_icon_width)));
 		mBuilder.setContentTitle(conversation.getName());
-		mBuilder.setTicker(conversation.getLatestMessage().trim());
+		mBuilder.setTicker(conversation.getLatestMessage().getBody().trim());
 		StringBuilder bigText = new StringBuilder();
 		List<Message> messages = conversation.getMessages();
 		String firstLine = "";

src/de/gultsch/chat/xml/TagWriter.java 🔗

@@ -3,12 +3,7 @@ package de.gultsch.chat.xml;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
 
 import android.util.Log;
 

src/de/gultsch/chat/xmpp/MessagePacket.java 🔗

@@ -42,6 +42,7 @@ public class MessagePacket extends Element {
 	}
 	
 	public void setBody(String text) {
+		this.children.remove(findChild("body"));
 		Element body = new Element("body");
 		body.setContent(text);
 		this.children.add(body);

src/de/gultsch/chat/xmpp/XmppConnection.java 🔗

@@ -411,6 +411,7 @@ public class XmppConnection implements Runnable {
 	}
 
 	public void sendMessagePacket(MessagePacket packet) {
+		Log.d(LOGTAG,"sending message packet "+packet.toString());
 		tagWriter.writeElement(packet);
 	}
 
@@ -440,6 +441,6 @@ public class XmppConnection implements Runnable {
 
 	public void disconnect() {
 		shouldConnect = false;
-		tagWriter.writeTag(Tag.end("stream"));
+		tagWriter.writeTag(Tag.end("stream:stream"));
 	}
 }