diff options
18 files changed, 611 insertions, 113 deletions
diff --git a/gen/de/gultsch/chat/R.java b/gen/de/gultsch/chat/R.java index d316fb902..6f1b50dee 100644 --- a/gen/de/gultsch/chat/R.java +++ b/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; diff --git a/libs/bcprov-jdk15on-150.jar b/libs/bcprov-jdk15on-150.jar Binary files differnew file mode 100644 index 000000000..d4b510d78 --- /dev/null +++ b/libs/bcprov-jdk15on-150.jar diff --git a/libs/otr4j-0.10.jar b/libs/otr4j-0.10.jar Binary files differnew file mode 100644 index 000000000..d4fc3a916 --- /dev/null +++ b/libs/otr4j-0.10.jar diff --git a/res/drawable-hdpi/ic_action_secure.png b/res/drawable-hdpi/ic_action_secure.png Binary files differnew file mode 100644 index 000000000..287ae2fb0 --- /dev/null +++ b/res/drawable-hdpi/ic_action_secure.png diff --git a/res/drawable-mdpi/ic_action_secure.png b/res/drawable-mdpi/ic_action_secure.png Binary files differnew file mode 100644 index 000000000..d49217234 --- /dev/null +++ b/res/drawable-mdpi/ic_action_secure.png diff --git a/res/drawable-xhdpi/ic_action_secure.png b/res/drawable-xhdpi/ic_action_secure.png Binary files differnew file mode 100644 index 000000000..2a0898381 --- /dev/null +++ b/res/drawable-xhdpi/ic_action_secure.png diff --git a/res/drawable-xxhdpi/ic_action_secure.png b/res/drawable-xxhdpi/ic_action_secure.png Binary files differnew file mode 100644 index 000000000..d8c094ed8 --- /dev/null +++ b/res/drawable-xxhdpi/ic_action_secure.png diff --git a/src/de/gultsch/chat/crypto/OtrEngine.java b/src/de/gultsch/chat/crypto/OtrEngine.java new file mode 100644 index 000000000..d994f0f9a --- /dev/null +++ b/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 + + } + +} diff --git a/src/de/gultsch/chat/entities/Account.java b/src/de/gultsch/chat/entities/Account.java index 0f7bfd52a..7f14b0901 100644 --- a/src/de/gultsch/chat/entities/Account.java +++ b/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; + } } diff --git a/src/de/gultsch/chat/entities/Conversation.java b/src/de/gultsch/chat/entities/Conversation.java index fba5464ad..7339aadb3 100644 --- a/src/de/gultsch/chat/entities/Conversation.java +++ b/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); + } } diff --git a/src/de/gultsch/chat/persistance/DatabaseBackend.java b/src/de/gultsch/chat/persistance/DatabaseBackend.java index 25a96f461..df919f728 100644 --- a/src/de/gultsch/chat/persistance/DatabaseBackend.java +++ b/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, " diff --git a/src/de/gultsch/chat/services/XmppConnectionService.java b/src/de/gultsch/chat/services/XmppConnectionService.java index 567c851f9..b1c4eca50 100644 --- a/src/de/gultsch/chat/services/XmppConnectionService.java +++ b/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 diff --git a/src/de/gultsch/chat/ui/ConversationActivity.java b/src/de/gultsch/chat/ui/ConversationActivity.java index 66cec2e7b..011261e7a 100644 --- a/src/de/gultsch/chat/ui/ConversationActivity.java +++ b/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); + } } } } diff --git a/src/de/gultsch/chat/ui/ConversationFragment.java b/src/de/gultsch/chat/ui/ConversationFragment.java index f7513d08b..8ec511fc2 100644 --- a/src/de/gultsch/chat/ui/ConversationFragment.java +++ b/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); diff --git a/src/de/gultsch/chat/utils/UIHelper.java b/src/de/gultsch/chat/utils/UIHelper.java index 8e6d8e5fd..df3f9c1cc 100644 --- a/src/de/gultsch/chat/utils/UIHelper.java +++ b/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 = ""; diff --git a/src/de/gultsch/chat/xml/TagWriter.java b/src/de/gultsch/chat/xml/TagWriter.java index 401a6eee2..844f5253e 100644 --- a/src/de/gultsch/chat/xml/TagWriter.java +++ b/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; diff --git a/src/de/gultsch/chat/xmpp/MessagePacket.java b/src/de/gultsch/chat/xmpp/MessagePacket.java index 5da76152f..0d4d07d7d 100644 --- a/src/de/gultsch/chat/xmpp/MessagePacket.java +++ b/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); diff --git a/src/de/gultsch/chat/xmpp/XmppConnection.java b/src/de/gultsch/chat/xmpp/XmppConnection.java index 337d8c7dd..1b9aa0c3a 100644 --- a/src/de/gultsch/chat/xmpp/XmppConnection.java +++ b/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")); } } |