basic otr support

This commit is contained in:
Daniel Gultsch 2014-02-13 23:40:08 +01:00
parent e63109215e
commit 42c4c1789a
18 changed files with 613 additions and 115 deletions

View File

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

BIN
libs/bcprov-jdk15on-150.jar Normal file

Binary file not shown.

BIN
libs/otr4j-0.10.jar Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

View File

@ -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
}
}

View File

@ -1,7 +1,14 @@
package de.gultsch.chat.entities; 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.ContentValues;
import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.util.JsonReader;
import android.util.Log; import android.util.Log;
public class Account extends AbstractEntity{ public class Account extends AbstractEntity{
@ -15,6 +22,7 @@ public class Account extends AbstractEntity{
public static final String PASSWORD = "password"; public static final String PASSWORD = "password";
public static final String OPTIONS = "options"; public static final String OPTIONS = "options";
public static final String ROSTERVERSION = "rosterversion"; public static final String ROSTERVERSION = "rosterversion";
public static final String KEYS = "keys";
public static final int OPTION_USETLS = 0; public static final int OPTION_USETLS = 0;
public static final int OPTION_DISABLED = 1; public static final int OPTION_DISABLED = 1;
@ -34,23 +42,32 @@ public class Account extends AbstractEntity{
protected String rosterVersion; protected String rosterVersion;
protected String resource; protected String resource;
protected int status = 0; protected int status = 0;
protected JSONObject keys = new JSONObject();
protected boolean online = false; protected boolean online = false;
transient OtrEngine otrEngine = null;
transient XmppConnection xmppConnection = null;
public Account() { public Account() {
this.uuid = "0"; this.uuid = "0";
} }
public Account(String username, String server, String password) { 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.uuid = uuid;
this.username = username; this.username = username;
this.server = server; this.server = server;
this.password = password; this.password = password;
this.options = options; this.options = options;
this.rosterVersion = rosterVersion; this.rosterVersion = rosterVersion;
try {
this.keys = new JSONObject(keys);
} catch (JSONException e) {
}
} }
public boolean isOptionSet(int option) { public boolean isOptionSet(int option) {
@ -108,6 +125,14 @@ public class Account extends AbstractEntity{
public String getJid() { public String getJid() {
return username+"@"+server; return username+"@"+server;
} }
public JSONObject getKeys() {
return keys;
}
public void setKey(String keyName, String keyValue) throws JSONException {
this.keys.put(keyName, keyValue);
}
@Override @Override
public ContentValues getContentValues() { public ContentValues getContentValues() {
@ -117,6 +142,8 @@ public class Account extends AbstractEntity{
values.put(SERVER, server); values.put(SERVER, server);
values.put(PASSWORD, password); values.put(PASSWORD, password);
values.put(OPTIONS,options); values.put(OPTIONS,options);
values.put(KEYS,this.keys.toString());
values.put(ROSTERVERSION,rosterVersion);
return values; return values;
} }
@ -126,8 +153,28 @@ public class Account extends AbstractEntity{
cursor.getString(cursor.getColumnIndex(SERVER)), cursor.getString(cursor.getColumnIndex(SERVER)),
cursor.getString(cursor.getColumnIndex(PASSWORD)), cursor.getString(cursor.getColumnIndex(PASSWORD)),
cursor.getInt(cursor.getColumnIndex(OPTIONS)), 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;
}
} }

View File

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

View File

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

View File

@ -6,6 +6,12 @@ import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.Hashtable; import java.util.Hashtable;
import java.util.List; 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.Account;
import de.gultsch.chat.entities.Contact; import de.gultsch.chat.entities.Contact;
@ -50,8 +56,6 @@ public class XmppConnectionService extends Service {
private List<Account> accounts; private List<Account> accounts;
private List<Conversation> conversations = null; private List<Conversation> conversations = null;
private Hashtable<Account, XmppConnection> connections = new Hashtable<Account, XmppConnection>();
private OnConversationListChangedListener convChangedListener = null; private OnConversationListChangedListener convChangedListener = null;
private OnAccountListChangedListener accountChangedListener = null; private OnAccountListChangedListener accountChangedListener = null;
@ -73,7 +77,9 @@ public class XmppConnectionService extends Service {
if ((packet.getType() == MessagePacket.TYPE_CHAT) if ((packet.getType() == MessagePacket.TYPE_CHAT)
|| (packet.getType() == MessagePacket.TYPE_GROUPCHAT)) { || (packet.getType() == MessagePacket.TYPE_GROUPCHAT)) {
boolean notify = true; boolean notify = true;
boolean runOtrCheck = false;
int status = Message.STATUS_RECIEVED; int status = Message.STATUS_RECIEVED;
int encryption = Message.ENCRYPTION_NONE;
String body; String body;
String fullJid; String fullJid;
if (!packet.hasChild("body")) { if (!packet.hasChild("body")) {
@ -106,6 +112,7 @@ public class XmppConnectionService extends Service {
} else { } else {
fullJid = packet.getFrom(); fullJid = packet.getFrom();
body = packet.getBody(); body = packet.getBody();
runOtrCheck = true;
} }
Conversation conversation = null; Conversation conversation = null;
String[] fromParts = fullJid.split("/"); String[] fromParts = fullJid.split("/");
@ -124,9 +131,51 @@ public class XmppConnectionService extends Service {
} }
} else { } else {
counterPart = fullJid; 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 message = new Message(conversation, counterPart, body,
Message.ENCRYPTION_NONE, status); encryption, status);
if (packet.hasChild("delay")) { if (packet.hasChild("delay")) {
try { try {
String stamp = packet.findChild("delay").getAttribute( String stamp = packet.findChild("delay").getAttribute(
@ -169,14 +218,14 @@ public class XmppConnectionService extends Service {
databaseBackend.clearPresences(account); databaseBackend.clearPresences(account);
connectMultiModeConversations(account); connectMultiModeConversations(account);
List<Conversation> conversations = getConversations(); List<Conversation> conversations = getConversations();
for(int i = 0; i < conversations.size(); ++i) { for (int i = 0; i < conversations.size(); ++i) {
if (conversations.get(i).getAccount()==account) { if (conversations.get(i).getAccount() == account) {
sendUnsendMessages(conversations.get(i)); sendUnsendMessages(conversations.get(i));
} }
} }
if (convChangedListener!=null) { if (convChangedListener != null) {
convChangedListener.onConversationListChanged(); convChangedListener.onConversationListChanged();
} }
} }
} }
}; };
@ -216,8 +265,19 @@ public class XmppConnectionService extends Service {
databaseBackend.updateContact(contact); 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 class XmppConnectionBinder extends Binder {
public XmppConnectionService getService() { public XmppConnectionService getService() {
@ -228,13 +288,9 @@ public class XmppConnectionService extends Service {
@Override @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
for (Account account : accounts) { for (Account account : accounts) {
if (!connections.containsKey(account)) { if (account.getXmppConnection() == null) {
if (!account.isOptionSet(Account.OPTION_DISABLED)) { if (!account.isOptionSet(Account.OPTION_DISABLED)) {
this.connections.put(account, account.setXmppConnection(this.createConnection(account));
this.createConnection(account));
} else {
Log.d(LOGTAG, account.getJid()
+ ": not starting because it's disabled");
} }
} }
} }
@ -250,6 +306,16 @@ public class XmppConnectionService extends Service {
ContactsContract.Contacts.CONTENT_URI, true, contactObserver); 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) { public XmppConnection createConnection(Account account) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
XmppConnection connection = new XmppConnection(account, pm); XmppConnection connection = new XmppConnection(account, pm);
@ -261,40 +327,105 @@ public class XmppConnectionService extends Service {
return connection; return connection;
} }
public void sendMessage(Account account, Message message) { private void startOtrSession(Conversation conv) {
Set<String> presences = conv.getContact().getPresences()
if (account.getStatus() == Account.STATUS_ONLINE) { .keySet();
MessagePacket packet = prepareMessagePacket(account, message); if (presences.size() == 0) {
connections.get(account).sendMessagePacket(packet); Log.d(LOGTAG, "counter part isnt online. cant use otr");
if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { return;
message.setStatus(Message.STATUS_SEND); } else if (presences.size() == 1) {
if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { conv.startOtrSession(getApplicationContext(),
databaseBackend.createMessage(message); (String) presences.toArray()[0]);
message.getConversation().getMessages().add(message); try {
if (convChangedListener!=null) { conv.getOtrSession().startSession();
convChangedListener.onConversationListChanged(); } 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;
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) {
message.setStatus(Message.STATUS_SEND);
saveInDb = true;
addToConversation = true;
}
packet = prepareMessagePacket(account, message, null);
account.getXmppConnection().sendMessagePacket(packet);
}
} else { } else {
message.getConversation().getMessages().add(message); // account is offline
saveInDb = true;
addToConversation = true;
}
if (saveInDb) {
databaseBackend.createMessage(message); databaseBackend.createMessage(message);
if (convChangedListener!=null) { }
if (addToConversation) {
conv.getMessages().add(message);
if (convChangedListener != null) {
convChangedListener.onConversationListChanged(); convChangedListener.onConversationListChanged();
} }
} }
} }
private void sendUnsendMessages(Conversation conversation) { private void sendUnsendMessages(Conversation conversation) {
for (int i = 0; i < conversation.getMessages().size(); ++i) { for (int i = 0; i < conversation.getMessages().size(); ++i) {
if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) { if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) {
Message message = conversation.getMessages() Message message = conversation.getMessages().get(i);
.get(i);
MessagePacket packet = prepareMessagePacket( MessagePacket packet = prepareMessagePacket(
conversation.getAccount(),message); conversation.getAccount(), message, null);
connections.get(conversation.getAccount()).sendMessagePacket( conversation.getAccount().getXmppConnection()
packet); .sendMessagePacket(packet);
message.setStatus(Message.STATUS_SEND); message.setStatus(Message.STATUS_SEND);
if (conversation.getMode() == Conversation.MODE_SINGLE) { if (conversation.getMode() == Conversation.MODE_SINGLE) {
databaseBackend.updateMessage(message); 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(); MessagePacket packet = new MessagePacket();
if (message.getConversation().getMode() == Conversation.MODE_SINGLE) { if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
packet.setType(MessagePacket.TYPE_CHAT); 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) { } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
packet.setType(MessagePacket.TYPE_GROUPCHAT); 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; return packet;
} }
@ -345,7 +497,7 @@ public class XmppConnectionService extends Service {
query.setAttribute("xmlns", "jabber:iq:roster"); query.setAttribute("xmlns", "jabber:iq:roster");
query.setAttribute("ver", ""); query.setAttribute("ver", "");
iqPacket.addChild(query); iqPacket.addChild(query);
connections.get(account).sendIqPacket(iqPacket, account.getXmppConnection().sendIqPacket(iqPacket,
new OnIqPacketReceived() { new OnIqPacketReceived() {
@Override @Override
@ -488,7 +640,7 @@ public class XmppConnectionService extends Service {
if (muc) { if (muc) {
conversation.setMode(Conversation.MODE_MULTI); conversation.setMode(Conversation.MODE_MULTI);
if (account.getStatus() == Account.STATUS_ONLINE) { if (account.getStatus() == Account.STATUS_ONLINE) {
joinMuc(account, conversation); joinMuc(conversation);
} }
} else { } else {
conversation.setMode(Conversation.MODE_SINGLE); conversation.setMode(Conversation.MODE_SINGLE);
@ -506,7 +658,7 @@ public class XmppConnectionService extends Service {
conversation = new Conversation(conversationName, account, jid, conversation = new Conversation(conversationName, account, jid,
Conversation.MODE_MULTI); Conversation.MODE_MULTI);
if (account.getStatus() == Account.STATUS_ONLINE) { if (account.getStatus() == Account.STATUS_ONLINE) {
joinMuc(account, conversation); joinMuc(conversation);
} }
} else { } else {
conversation = new Conversation(conversationName, account, jid, conversation = new Conversation(conversationName, account, jid,
@ -523,6 +675,17 @@ public class XmppConnectionService extends Service {
} }
public void archiveConversation(Conversation 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.databaseBackend.updateConversation(conversation);
this.conversations.remove(conversation); this.conversations.remove(conversation);
if (this.convChangedListener != null) { if (this.convChangedListener != null) {
@ -537,23 +700,18 @@ public class XmppConnectionService extends Service {
public void createAccount(Account account) { public void createAccount(Account account) {
databaseBackend.createAccount(account); databaseBackend.createAccount(account);
this.accounts.add(account); this.accounts.add(account);
this.connections.put(account, this.createConnection(account)); account.setXmppConnection(this.createConnection(account));
if (accountChangedListener != null) if (accountChangedListener != null)
accountChangedListener.onAccountListChangedListener(); accountChangedListener.onAccountListChangedListener();
} }
public void updateAccount(Account account) { public void updateAccount(Account account) {
databaseBackend.updateAccount(account); databaseBackend.updateAccount(account);
XmppConnection connection = this.connections.get(account); if (account.getXmppConnection() != null) {
if (connection != null) { disconnect(account);
connection.disconnect();
this.connections.remove(account);
} }
if (!account.isOptionSet(Account.OPTION_DISABLED)) { if (!account.isOptionSet(Account.OPTION_DISABLED)) {
this.connections.put(account, this.createConnection(account)); account.setXmppConnection(this.createConnection(account));
} else {
Log.d(LOGTAG, account.getJid()
+ ": not starting because it's disabled");
} }
if (accountChangedListener != null) if (accountChangedListener != null)
accountChangedListener.onAccountListChangedListener(); accountChangedListener.onAccountListChangedListener();
@ -561,10 +719,8 @@ public class XmppConnectionService extends Service {
public void deleteAccount(Account account) { public void deleteAccount(Account account) {
Log.d(LOGTAG, "called delete account"); Log.d(LOGTAG, "called delete account");
if (this.connections.containsKey(account)) { if (account.getXmppConnection() != null) {
Log.d(LOGTAG, "found connection. disconnecting"); this.disconnect(account);
this.connections.get(account).disconnect();
this.connections.remove(account);
} }
databaseBackend.deleteAccount(account); databaseBackend.deleteAccount(account);
this.accounts.remove(account); this.accounts.remove(account);
@ -596,33 +752,56 @@ public class XmppConnectionService extends Service {
Conversation conversation = conversations.get(i); Conversation conversation = conversations.get(i);
if ((conversation.getMode() == Conversation.MODE_MULTI) if ((conversation.getMode() == Conversation.MODE_MULTI)
&& (conversation.getAccount() == account)) { && (conversation.getAccount() == account)) {
joinMuc(account, conversation); joinMuc(conversation);
} }
} }
} }
public void joinMuc(Account account, Conversation conversation) { public void joinMuc(Conversation conversation) {
String muc = conversation.getContactJid(); String muc = conversation.getContactJid();
PresencePacket packet = new PresencePacket(); PresencePacket packet = new PresencePacket();
packet.setAttribute("to", muc + "/" + account.getUsername()); packet.setAttribute("to", muc + "/"
+ conversation.getAccount().getUsername());
Element x = new Element("x"); Element x = new Element("x");
x.setAttribute("xmlns", "http://jabber.org/protocol/muc"); x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
if (conversation.getMessages().size() != 0) { if (conversation.getMessages().size() != 0) {
Element history = new Element("history"); Element history = new Element("history");
history.setAttribute( history.setAttribute("seconds",
"seconds",
(System.currentTimeMillis() - conversation (System.currentTimeMillis() - conversation
.getLatestMessageDate()) / 1000 + ""); .getLatestMessage().getTimeSent() / 1000) + "");
x.addChild(history); x.addChild(history);
} }
packet.addChild(x); 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 @Override
public IBinder onBind(Intent intent) { public IBinder onBind(Intent intent) {
return mBinder; return mBinder;

View File

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

View File

@ -15,7 +15,6 @@ import android.graphics.Typeface;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
@ -36,38 +35,30 @@ public class ConversationFragment extends Fragment {
protected ArrayAdapter<Message> messageListAdapter; protected ArrayAdapter<Message> messageListAdapter;
protected Contact contact; protected Contact contact;
private EditText chatMsg;
private int nextMessageEncryption = Message.ENCRYPTION_NONE;
@Override @Override
public View onCreateView(final LayoutInflater inflater, public View onCreateView(final LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) { ViewGroup container, Bundle savedInstanceState) {
this.inflater = inflater; this.inflater = inflater;
final View view = inflater.inflate(R.layout.fragment_conversation, final View view = inflater.inflate(R.layout.fragment_conversation,
container, false); container, false);
chatMsg = (EditText) view.findViewById(R.id.textinput);
((ImageButton) view.findViewById(R.id.textSendButton)) ((ImageButton) view.findViewById(R.id.textSendButton))
.setOnClickListener(new OnClickListener() { .setOnClickListener(new OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
ConversationActivity activity = (ConversationActivity) getActivity(); ConversationActivity activity = (ConversationActivity) getActivity();
EditText chatMsg = (EditText) view
.findViewById(R.id.textinput);
if (chatMsg.getText().length() < 1) if (chatMsg.getText().length() < 1)
return; return;
Message message = new Message(conversation, chatMsg Message message = new Message(conversation, chatMsg
.getText().toString(), Message.ENCRYPTION_NONE); .getText().toString(), nextMessageEncryption);
activity.xmppConnectionService.sendMessage(conversation.getAccount(),message); activity.xmppConnectionService.sendMessage(conversation.getAccount(),message);
chatMsg.setText(""); 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.clear();
this.messageList.addAll(this.conversation.getMessages()); this.messageList.addAll(this.conversation.getMessages());
this.messageListAdapter.notifyDataSetChanged(); 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(); int size = this.messageList.size();
if (size >= 1) if (size >= 1)
messagesView.setSelection(size - 1); messagesView.setSelection(size - 1);

View File

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

View File

@ -3,12 +3,7 @@ package de.gultsch.chat.xml;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.OutputStreamWriter; 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.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import android.util.Log; import android.util.Log;

View File

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

View File

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