roster versioning. roster updates on air. performance fixes in conversation listview

Daniel Gultsch created

Change summary

res/layout/dialog_verify_otr.xml                           |  10 
res/layout/fragment_conversation.xml                       |   3 
src/de/gultsch/chat/entities/Account.java                  |  12 
src/de/gultsch/chat/entities/Contact.java                  |   3 
src/de/gultsch/chat/persistance/DatabaseBackend.java       |  18 
src/de/gultsch/chat/persistance/OnPhoneContactsMerged.java |   5 
src/de/gultsch/chat/services/XmppConnectionService.java    | 390 +++----
src/de/gultsch/chat/ui/ConversationFragment.java           | 205 ++-
src/de/gultsch/chat/utils/MessageParser.java               | 119 ++
src/de/gultsch/chat/utils/PhoneHelper.java                 |  11 
src/de/gultsch/chat/xml/XmlReader.java                     |   4 
11 files changed, 491 insertions(+), 289 deletions(-)

Detailed changes

res/layout/dialog_verify_otr.xml 🔗

@@ -3,7 +3,9 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
-    android:paddingLeft="8dp">
+    android:paddingLeft="8dp"
+    android:paddingBottom="16dp"
+    android:paddingRight="8dp">
     
  <TextView
         android:layout_width="wrap_content"
@@ -34,7 +36,8 @@
         android:layout_height="wrap_content"
         android:paddingLeft="8dp"
         android:text="2674D6A0 0B1421B1 BFC42AEC C56F3719 672437D8"
-        android:textSize="14sp" />
+        android:textSize="14sp"
+        android:typeface="monospace"/>
      <TextView
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
@@ -49,5 +52,6 @@
         android:layout_height="wrap_content"
         android:paddingLeft="8dp"
         android:text="2674D6A0 0B1421B1 BFC42AEC C56F3719 672437D8"
-        android:textSize="14sp" />
+        android:textSize="14sp"
+        android:typeface="monospace"/>
 </LinearLayout>

res/layout/fragment_conversation.xml 🔗

@@ -80,7 +80,8 @@
             android:text="2674D6A0 0B1421B1 BFC42AEC C56F3719 672437D8"
             android:paddingLeft="8dp"
             android:paddingBottom="8dp"
-            android:textSize="14sp"/>
+            android:textSize="14sp"
+            android:typeface="monospace"/>
         
     </LinearLayout>
 

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

@@ -201,4 +201,16 @@ public class Account  extends AbstractEntity{
 		}
 		return this.otrFingerprint;
 	}
+
+	public String getRosterVersion() {
+		if (this.rosterVersion==null) {
+			return "";
+		} else {
+			return this.rosterVersion;
+		}
+	}
+	
+	public void setRosterVersion(String version) {
+		this.rosterVersion = version;
+	}
 }

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

@@ -32,7 +32,7 @@ public class Contact extends AbstractEntity implements Serializable {
 	protected String subscription;
 	protected String systemAccount;
 	protected String photoUri;
-	protected JSONObject keys;
+	protected JSONObject keys = new JSONObject();
 	protected Presences presences = new Presences();
 
 	protected Account account;
@@ -47,6 +47,7 @@ public class Contact extends AbstractEntity implements Serializable {
 		this.displayName = displayName;
 		this.jid = jid;
 		this.photoUri = photoUri;
+		this.uuid = java.util.UUID.randomUUID().toString();
 	}
 
 	public Contact(String uuid, String account, String displayName, String jid,

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

@@ -245,6 +245,16 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		}
 		return list;
 	}
+	
+	public List<Contact> getContats(String where) {
+		List<Contact> list = new ArrayList<Contact>();
+		SQLiteDatabase db = this.getReadableDatabase();
+		Cursor cursor = db.query(Contact.TABLENAME, null, where, null, null, null, null);
+		while (cursor.moveToNext()) {
+			list.add(Contact.fromCursor(cursor));
+		}
+		return list;
+	}
 
 	public Contact findContact(Account account, String jid) {
 		SQLiteDatabase db = this.getReadableDatabase();
@@ -263,4 +273,12 @@ public class DatabaseBackend extends SQLiteOpenHelper {
 		String[] args = { message.getUuid() };
 		db.delete(Message.TABLENAME, Message.UUID + "=?", args);
 	}
+
+	public void deleteContact(Contact contact) {
+		SQLiteDatabase db = this.getWritableDatabase();
+		String[] args = { contact.getUuid() };
+		db.delete(Contact.TABLENAME, Contact.UUID + "=?", args);
+	}
+
+	
 }

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

@@ -19,9 +19,11 @@ import de.gultsch.chat.entities.Conversation;
 import de.gultsch.chat.entities.Message;
 import de.gultsch.chat.entities.Presences;
 import de.gultsch.chat.persistance.DatabaseBackend;
+import de.gultsch.chat.persistance.OnPhoneContactsMerged;
 import de.gultsch.chat.ui.OnAccountListChangedListener;
 import de.gultsch.chat.ui.OnConversationListChangedListener;
 import de.gultsch.chat.ui.OnRosterFetchedListener;
+import de.gultsch.chat.utils.MessageParser;
 import de.gultsch.chat.utils.OnPhoneContactsLoadedListener;
 import de.gultsch.chat.utils.PhoneHelper;
 import de.gultsch.chat.utils.UIHelper;
@@ -39,6 +41,7 @@ import android.app.Service;
 import android.content.Context;
 import android.content.Intent;
 import android.database.ContentObserver;
+import android.database.DatabaseUtils;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
@@ -49,14 +52,14 @@ import android.util.Log;
 public class XmppConnectionService extends Service {
 
 	protected static final String LOGTAG = "xmppService";
-	protected DatabaseBackend databaseBackend;
+	public DatabaseBackend databaseBackend;
 
 	public long startDate;
 
 	private List<Account> accounts;
 	private List<Conversation> conversations = null;
 
-	private OnConversationListChangedListener convChangedListener = null;
+	public OnConversationListChangedListener convChangedListener = null;
 	private OnAccountListChangedListener accountChangedListener = null;
 
 	private ContentObserver contactObserver = new ContentObserver(null) {
@@ -64,148 +67,74 @@ public class XmppConnectionService extends Service {
 		public void onChange(boolean selfChange) {
 			super.onChange(selfChange);
 			Log.d(LOGTAG, "contact list has changed");
-			mergePhoneContactsWithRoster();
+			mergePhoneContactsWithRoster(null);
 		}
 	};
 
+	private XmppConnectionService service = this;
+
 	private final IBinder mBinder = new XmppConnectionBinder();
 	private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
 
 		@Override
 		public void onMessagePacketReceived(Account account,
 				MessagePacket packet) {
-			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")) {
-					Element forwarded;
-					if (packet.hasChild("received")) {
-						forwarded = packet.findChild("received").findChild(
-								"forwarded");
-					} else if (packet.hasChild("sent")) {
-						forwarded = packet.findChild("sent").findChild(
-								"forwarded");
-						status = Message.STATUS_SEND;
-						notify = false;
-					} else {
-						return; // massage has no body and is not carbon. just
-						// skip
-					}
-					if (forwarded != null) {
-						Element message = forwarded.findChild("message");
-						if ((message == null) || (!message.hasChild("body")))
-							return; // either malformed or boring
-						if (status == Message.STATUS_RECIEVED) {
-							fullJid = message.getAttribute("from");
-						} else {
-							fullJid = message.getAttribute("to");
-						}
-						body = message.findChild("body").getContent();
-					} else {
-						return; // packet malformed. has no forwarded element
-					}
-				} else {
-					fullJid = packet.getFrom();
-					body = packet.getBody();
-					runOtrCheck = true;
+			Message message = null;
+			boolean notify = false;
+			if ((packet.getType() == MessagePacket.TYPE_CHAT)) {
+				if (packet.hasChild("body")
+						&& (packet.getBody().startsWith("?OTR"))) {
+					message = MessageParser.parseOtrChat(packet, account,
+							service);
+					notify = true;
+				} else if (packet.hasChild("body")) {
+					message = MessageParser.parsePlainTextChat(packet, account,
+							service);
+					notify = true;
+				} else if (packet.hasChild("received")
+						|| (packet.hasChild("sent"))) {
+					message = MessageParser.parseCarbonMessage(packet, account,
+							service);
 				}
-				Conversation conversation = null;
-				String[] fromParts = fullJid.split("/");
-				String jid = fromParts[0];
-				boolean muc = (packet.getType() == MessagePacket.TYPE_GROUPCHAT);
-				String counterPart = null;
-				conversation = findOrCreateConversation(account, jid, muc);
-				if (muc) {
-					if ((fromParts.length == 1) || (packet.hasChild("subject"))) {
-						return;
-					}
-					counterPart = fromParts[1];
-					if (counterPart.equals(account.getUsername())) {
-						status = Message.STATUS_SEND;
-						notify = false;
-					}
-				} else {
-					counterPart = fullJid;
-					if ((runOtrCheck) && body.startsWith("?OTR")) {
-						if (!conversation.hasValidOtrSession()) {
-							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();
-								}
-							} else if ((before != after) && (after == SessionStatus.FINISHED)) {
-								conversation.resetOtrSession();
-								Log.d(LOGTAG,"otr session stoped");
-							}
-						} catch (Exception e) {
-							Log.d(LOGTAG, "error receiving otr. resetting");
-							conversation.resetOtrSession();
-							return;
-						}
-						if (body == null) {
-							return;
-						}
-						encryption = Message.ENCRYPTION_OTR;
-					}
+
+			} else if (packet.getType() == MessagePacket.TYPE_GROUPCHAT) {
+				message = MessageParser
+						.parseGroupchat(packet, account, service);
+				if (message != null) {
+					notify = (message.getStatus() == Message.STATUS_RECIEVED);
 				}
-				Message message = new Message(conversation, counterPart, body,
-						encryption, status);
-				if (packet.hasChild("delay")) {
-					try {
-						String stamp = packet.findChild("delay").getAttribute(
-								"stamp");
-						stamp = stamp.replace("Z", "+0000");
-						Date date = new SimpleDateFormat(
-								"yyyy-MM-dd'T'HH:mm:ssZ").parse(stamp);
-						message.setTime(date.getTime());
-					} catch (ParseException e) {
-						Log.d(LOGTAG,
-								"error trying to parse date" + e.getMessage());
-					}
+			} else {
+				Log.d(LOGTAG, "unparsed message " + packet.toString());
+			}
+			if (message == null) {
+				return;
+			}
+			if (packet.hasChild("delay")) {
+				try {
+					String stamp = packet.findChild("delay").getAttribute(
+							"stamp");
+					stamp = stamp.replace("Z", "+0000");
+					Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
+							.parse(stamp);
+					message.setTime(date.getTime());
+				} catch (ParseException e) {
+					Log.d(LOGTAG, "error trying to parse date" + e.getMessage());
 				}
+			}
+			if (notify) {
+				message.markUnread();
+			}
+			Conversation conversation = message.getConversation();
+			conversation.getMessages().add(message);
+			databaseBackend.createMessage(message);
+			if (convChangedListener != null) {
+				convChangedListener.onConversationListChanged();
+			} else {
 				if (notify) {
-					message.markUnread();
-				}
-				conversation.getMessages().add(message);
-				databaseBackend.createMessage(message);
-				if (convChangedListener != null) {
-					convChangedListener.onConversationListChanged();
-				} else {
-					if (notify) {
-						NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
-						mNotificationManager.notify(2342, UIHelper
-								.getUnreadMessageNotification(
-										getApplicationContext(), conversation));
-					}
+					NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+					mNotificationManager.notify(2342, UIHelper
+							.getUnreadMessageNotification(
+									getApplicationContext(), conversation));
 				}
 			}
 		}
@@ -271,11 +200,54 @@ public class XmppConnectionService extends Service {
 			replaceContactInConversation(contact);
 		}
 	};
-	
+
+	private OnIqPacketReceived unknownIqListener = new OnIqPacketReceived() {
+
+		@Override
+		public void onIqPacketReceived(Account account, IqPacket packet) {
+			if (packet.hasChild("query")) {
+				Element query = packet.findChild("query");
+				String xmlns = query.getAttribute("xmlns");
+				if ((xmlns != null) && (xmlns.equals("jabber:iq:roster"))) {
+					processRosterItems(account, query);
+					mergePhoneContactsWithRoster(null);
+				}
+			}
+		}
+	};
+
+	private void processRosterItems(Account account, Element elements) {
+		for (Element item : elements.getChildren()) {
+			if (item.getName().equals("item")) {
+				String jid = item.getAttribute("jid");
+				String subscription = item.getAttribute("subscription");
+				Contact contact = databaseBackend.findContact(account, jid);
+				if (contact == null) {
+					String name = item.getAttribute("name");
+					if (name == null) {
+						name = jid.split("@")[0];
+					}
+					contact = new Contact(account, name, jid, null);
+					contact.setSubscription(subscription);
+					databaseBackend.createContact(contact);
+				} else {
+					if (subscription.equals("remove")) {
+						databaseBackend.deleteContact(contact);
+					} else {
+						contact.setSubscription(subscription);
+						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)) {
+		for (int i = 0; i < conversations.size(); ++i) {
+			if ((conversations.get(i).getContact() != null)
+					&& (conversations.get(i).getContact().equals(contact))) {
 				conversations.get(i).setContact(contact);
 				break;
 			}
@@ -325,11 +297,13 @@ public class XmppConnectionService extends Service {
 		connection.setOnMessagePacketReceivedListener(this.messageListener);
 		connection.setOnStatusChangedListener(this.statusListener);
 		connection.setOnPresencePacketReceivedListener(this.presenceListener);
+		connection
+				.setOnUnregisteredIqPacketReceivedListener(this.unknownIqListener);
 		Thread thread = new Thread(connection);
 		thread.start();
 		return connection;
 	}
-	
+
 	public void sendMessage(Account account, Message message, String presence) {
 		Conversation conv = message.getConversation();
 		boolean saveInDb = false;
@@ -338,10 +312,11 @@ public class XmppConnectionService extends Service {
 			MessagePacket packet;
 			if (message.getEncryption() == Message.ENCRYPTION_OTR) {
 				if (!conv.hasValidOtrSession()) {
-					//starting otr session. messages will be send later
+					// starting otr session. messages will be send later
 					conv.startOtrSession(getApplicationContext(), presence);
-				} else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED){
-					//otr session aleary exists, creating message packet accordingly
+				} else if (conv.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) {
+					// otr session aleary exists, creating message packet
+					// accordingly
 					packet = prepareMessagePacket(account, message,
 							conv.getOtrSession());
 					account.getXmppConnection().sendMessagePacket(packet);
@@ -356,7 +331,7 @@ public class XmppConnectionService extends Service {
 					saveInDb = true;
 					addToConversation = true;
 				}
-				
+
 				packet = prepareMessagePacket(account, message, null);
 				account.getXmppConnection().sendMessagePacket(packet);
 			}
@@ -398,8 +373,8 @@ public class XmppConnectionService extends Service {
 		}
 	}
 
-	private MessagePacket prepareMessagePacket(Account account,
-			Message message, Session otrSession) {
+	public MessagePacket prepareMessagePacket(Account account, Message message,
+			Session otrSession) {
 		MessagePacket packet = new MessagePacket();
 		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
 			packet.setType(MessagePacket.TYPE_CHAT);
@@ -416,7 +391,8 @@ public class XmppConnectionService extends Service {
 				Element privateMarker = new Element("private");
 				privateMarker.setAttribute("xmlns", "urn:xmpp:carbons:2");
 				packet.addChild(privateMarker);
-				packet.setTo(otrSession.getSessionID().getAccountID()+"/"+otrSession.getSessionID().getUserID());
+				packet.setTo(otrSession.getSessionID().getAccountID() + "/"
+						+ otrSession.getSessionID().getUserID());
 				packet.setFrom(account.getFullJid());
 			} else {
 				packet.setBody(message.getBody());
@@ -445,81 +421,65 @@ public class XmppConnectionService extends Service {
 
 	public void updateRoster(final Account account,
 			final OnRosterFetchedListener listener) {
-
-		PhoneHelper.loadPhoneContacts(this,
-				new OnPhoneContactsLoadedListener() {
+		IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
+		Element query = new Element("query");
+		query.setAttribute("xmlns", "jabber:iq:roster");
+		query.setAttribute("ver", account.getRosterVersion());
+		iqPacket.addChild(query);
+		account.getXmppConnection().sendIqPacket(iqPacket,
+				new OnIqPacketReceived() {
 
 					@Override
-					public void onPhoneContactsLoaded(
-							final Hashtable<String, Bundle> phoneContacts) {
-						IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
-						Element query = new Element("query");
-						query.setAttribute("xmlns", "jabber:iq:roster");
-						query.setAttribute("ver", "");
-						iqPacket.addChild(query);
-						account.getXmppConnection().sendIqPacket(iqPacket,
-								new OnIqPacketReceived() {
-
-									@Override
-									public void onIqPacketReceived(
-											Account account, IqPacket packet) {
-										List<Contact> contacts = new ArrayList<Contact>();
-										Element roster = packet
-												.findChild("query");
-										if (roster != null) {
-											for (Element item : roster
-													.getChildren()) {
-												Contact contact;
-												String name = item
-														.getAttribute("name");
-												String jid = item
-														.getAttribute("jid");
-												if (phoneContacts
-														.containsKey(jid)) {
-													Bundle phoneContact = phoneContacts
-															.get(jid);
-													String systemAccount = phoneContact
-															.getInt("phoneid")
-															+ "#"
-															+ phoneContact
-																	.getString("lookup");
-													contact = new Contact(
-															account,
-															phoneContact
-																	.getString("displayname"),
-															jid,
-															phoneContact
-																	.getString("photouri"));
-													contact.setSystemAccount(systemAccount);
-												} else {
-													if (name == null) {
-														name = jid.split("@")[0];
-													}
-													contact = new Contact(
-															account, name, jid,
-															null);
-
-												}
-												contact.setAccount(account);
-												contact.setSubscription(item
-														.getAttribute("subscription"));
-												contacts.add(contact);
-											}
-											databaseBackend
-													.mergeContacts(contacts);
-											if (listener != null) {
-												listener.onRosterFetched(contacts);
-											}
-										}
+					public void onIqPacketReceived(final Account account,
+							IqPacket packet) {
+						Element roster = packet.findChild("query");
+						if (roster != null) {
+							String version = roster.getAttribute("ver");
+							processRosterItems(account, roster);
+							if (version!=null) {
+								account.setRosterVersion(version);
+								databaseBackend.updateAccount(account);
+							} else {
+								StringBuilder mWhere = new StringBuilder();
+								mWhere.append("jid NOT IN(");
+								List<Element> items = roster.getChildren();
+								for(int i = 0; i < items.size(); ++i) {
+									mWhere.append("\"");
+									mWhere.append(DatabaseUtils.sqlEscapeString(items.get(i).getAttribute("jid")));
+									if (i != items.size() - 1) {
+										mWhere.append("\",");
+									} else {
+										mWhere.append("\"");
 									}
-								});
-
+								}
+								mWhere.append(") and accountUuid = \"");
+								mWhere.append(account.getUuid());
+								mWhere.append("\"");
+								List<Contact> contactsToDelete = databaseBackend.getContats(mWhere.toString());
+								for(Contact contact : contactsToDelete) {
+									databaseBackend.deleteContact(contact);
+								}
+							}
+							mergePhoneContactsWithRoster(new OnPhoneContactsMerged() {
+								
+								@Override
+								public void phoneContactsMerged() {
+									if (listener != null) {
+										getRoster(account, listener);
+									}
+								}
+							});
+						} else {
+							if (listener != null) {
+								getRoster(account, listener);
+							}
+						}
 					}
 				});
 	}
 
-	public void mergePhoneContactsWithRoster() {
-		PhoneHelper.loadPhoneContacts(this,
+	public void mergePhoneContactsWithRoster(final OnPhoneContactsMerged listener) {
+		PhoneHelper.loadPhoneContacts(getApplicationContext(),
 				new OnPhoneContactsLoadedListener() {
 					@Override
 					public void onPhoneContactsLoaded(
@@ -550,6 +510,9 @@ public class XmppConnectionService extends Service {
 								}
 							}
 						}
+						if (listener!=null) {
+							listener.phoneContactsMerged();
+						}
 					}
 				});
 	}
@@ -606,7 +569,8 @@ public class XmppConnectionService extends Service {
 				conversation.setMode(Conversation.MODE_SINGLE);
 			}
 			this.databaseBackend.updateConversation(conversation);
-			conversation.setContact(findContact(account, conversation.getContactJid()));
+			conversation.setContact(findContact(account,
+					conversation.getContactJid()));
 		} else {
 			String conversationName;
 			Contact contact = findContact(account, jid);
@@ -728,8 +692,8 @@ public class XmppConnectionService extends Service {
 		if (conversation.getMessages().size() != 0) {
 			Element history = new Element("history");
 			long lastMsgTime = conversation.getLatestMessage().getTimeSent();
-			long diff = (System.currentTimeMillis() - lastMsgTime) / 1000;
-			history.setAttribute("seconds",diff+"");
+			long diff = (System.currentTimeMillis() - lastMsgTime) / 1000 - 1;
+			history.setAttribute("seconds", diff + "");
 			x.addChild(history);
 		}
 		packet.addChild(x);

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

@@ -1,6 +1,9 @@
 package de.gultsch.chat.ui;
 
+import java.io.FileNotFoundException;
 import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Hashtable;
 import java.util.List;
 import java.util.Set;
@@ -19,13 +22,17 @@ import android.app.AlertDialog;
 import android.app.Fragment;
 import android.content.DialogInterface;
 import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
 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;
+import android.view.View.OnLayoutChangeListener;
 import android.view.ViewGroup;
 import android.widget.ArrayAdapter;
 import android.widget.EditText;
@@ -43,6 +50,7 @@ public class ConversationFragment extends Fragment {
 	protected List<Message> messageList = new ArrayList<Message>();
 	protected ArrayAdapter<Message> messageListAdapter;
 	protected Contact contact;
+	protected BitmapCache mBitmapCache = new BitmapCache();
 
 	private EditText chatMsg;
 
@@ -104,13 +112,24 @@ public class ConversationFragment extends Fragment {
 		boolean showPhoneSelfContactPicture = sharedPref.getBoolean(
 				"show_phone_selfcontact_picture", true);
 
-		final Uri selfiUri;
+		Bitmap self;
+
 		if (showPhoneSelfContactPicture) {
-			selfiUri = PhoneHelper.getSefliUri(getActivity());
+			Uri selfiUri = PhoneHelper.getSefliUri(getActivity());
+			try {
+				self = BitmapFactory.decodeStream(getActivity()
+						.getContentResolver().openInputStream(selfiUri));
+			} catch (FileNotFoundException e) {
+				self = UIHelper.getUnknownContactPicture(conversation
+						.getAccount().getJid(), 200);
+			}
 		} else {
-			selfiUri = null;
+			self = UIHelper.getUnknownContactPicture(conversation.getAccount()
+					.getJid(), 200);
 		}
 
+		final Bitmap selfBitmap = self;
+
 		messageListAdapter = new ArrayAdapter<Message>(this.getActivity()
 				.getApplicationContext(), R.layout.message_sent,
 				this.messageList) {
@@ -136,68 +155,73 @@ public class ConversationFragment extends Fragment {
 			public View getView(int position, View view, ViewGroup parent) {
 				Message item = getItem(position);
 				int type = getItemViewType(position);
+				ViewHolder viewHolder;
 				if (view == null) {
 					switch (type) {
 					case SENT:
+						viewHolder = new ViewHolder();
 						view = (View) inflater.inflate(R.layout.message_sent,
 								null);
+						viewHolder.imageView = (ImageView) view
+								.findViewById(R.id.message_photo);
+						viewHolder.messageBody = (TextView) view
+								.findViewById(R.id.message_body);
+						viewHolder.time = (TextView) view
+								.findViewById(R.id.message_time);
+						view.setTag(viewHolder);
 						break;
 					case RECIEVED:
+						viewHolder = new ViewHolder();
 						view = (View) inflater.inflate(
 								R.layout.message_recieved, null);
+						viewHolder.imageView = (ImageView) view
+								.findViewById(R.id.message_photo);
+						viewHolder.messageBody = (TextView) view
+								.findViewById(R.id.message_body);
+						viewHolder.time = (TextView) view
+								.findViewById(R.id.message_time);
+						view.setTag(viewHolder);
+						break;
+					default:
+						viewHolder = null;
 						break;
 					}
+				} else {
+					viewHolder = (ViewHolder) view.getTag();
 				}
-				ImageView imageView = (ImageView) view
-						.findViewById(R.id.message_photo);
 				if (type == RECIEVED) {
 					if (item.getConversation().getMode() == Conversation.MODE_SINGLE) {
 						Uri uri = item.getConversation().getProfilePhotoUri();
 						if (uri != null) {
-							imageView.setImageURI(uri);
+							viewHolder.imageView.setImageBitmap(mBitmapCache.get(item.getConversation().getName(), uri));
 						} else {
-							imageView.setImageBitmap(UIHelper
-									.getUnknownContactPicture(item
-											.getConversation().getName(), 200));
+							viewHolder.imageView.setImageBitmap(mBitmapCache.get(item.getConversation().getName(),null));
 						}
 					} else if (item.getConversation().getMode() == Conversation.MODE_MULTI) {
 						if (item.getCounterpart() != null) {
-							imageView.setImageBitmap(UIHelper
-									.getUnknownContactPicture(
-											item.getCounterpart(), 200));
+							viewHolder.imageView.setImageBitmap(mBitmapCache.get(item.getCounterpart(),null));
 						} else {
-							imageView.setImageBitmap(UIHelper
-									.getUnknownContactPicture(item
-											.getConversation().getName(), 200));
+							viewHolder.imageView.setImageBitmap(mBitmapCache.get(item.getConversation().getName(),null));
 						}
 					}
 				} else {
-					if (selfiUri != null) {
-						imageView.setImageURI(selfiUri);
-					} else {
-						imageView.setImageBitmap(UIHelper
-								.getUnknownContactPicture(conversation
-										.getAccount().getJid(), 200));
-					}
+					viewHolder.imageView.setImageBitmap(selfBitmap);
 				}
-				TextView messageBody = (TextView) view
-						.findViewById(R.id.message_body);
 				String body = item.getBody();
 				if (body != null) {
-					messageBody.setText(body.trim());
+					viewHolder.messageBody.setText(body.trim());
 				}
-				TextView time = (TextView) view.findViewById(R.id.message_time);
 				if (item.getStatus() == Message.STATUS_UNSEND) {
-					time.setTypeface(null, Typeface.ITALIC);
-					time.setText("sending\u2026");
+					viewHolder.time.setTypeface(null, Typeface.ITALIC);
+					viewHolder.time.setText("sending\u2026");
 				} else {
-					time.setTypeface(null, Typeface.NORMAL);
+					viewHolder.time.setTypeface(null, Typeface.NORMAL);
 					if ((item.getConversation().getMode() == Conversation.MODE_SINGLE)
 							|| (type != RECIEVED)) {
-						time.setText(UIHelper.readableTimeDifference(item
-								.getTimeSent()));
+						viewHolder.time.setText(UIHelper
+								.readableTimeDifference(item.getTimeSent()));
 					} else {
-						time.setText(item.getCounterpart()
+						viewHolder.time.setText(item.getCounterpart()
 								+ " \u00B7 "
 								+ UIHelper.readableTimeDifference(item
 										.getTimeSent()));
@@ -275,24 +299,31 @@ public class ConversationFragment extends Fragment {
 	protected void makeFingerprintWarning(int latestEncryption) {
 		final LinearLayout fingerprintWarning = (LinearLayout) getView()
 				.findViewById(R.id.new_fingerprint);
-		Set<String> knownFingerprints = conversation.getContact()
-				.getOtrFingerprints();
-		if ((latestEncryption == Message.ENCRYPTION_OTR)
-				&& (conversation.hasValidOtrSession()
-						&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
-							.contains(conversation.getOtrFingerprint())))) {
-			fingerprintWarning.setVisibility(View.VISIBLE);
-			TextView fingerprint = (TextView) getView().findViewById(
-					R.id.otr_fingerprint);
-			fingerprint.setText(conversation.getOtrFingerprint());
-			fingerprintWarning.setOnClickListener(new OnClickListener() {
-
-				@Override
-				public void onClick(View v) {
-					AlertDialog dialog = UIHelper.getVerifyFingerprintDialog((ConversationActivity) getActivity(),conversation,fingerprintWarning);
-					dialog.show();
-				}
-			});
+		if (conversation.getContact() != null) {
+			Set<String> knownFingerprints = conversation.getContact()
+					.getOtrFingerprints();
+			if ((latestEncryption == Message.ENCRYPTION_OTR)
+					&& (conversation.hasValidOtrSession()
+							&& (conversation.getOtrSession().getSessionStatus() == SessionStatus.ENCRYPTED) && (!knownFingerprints
+								.contains(conversation.getOtrFingerprint())))) {
+				fingerprintWarning.setVisibility(View.VISIBLE);
+				TextView fingerprint = (TextView) getView().findViewById(
+						R.id.otr_fingerprint);
+				fingerprint.setText(conversation.getOtrFingerprint());
+				fingerprintWarning.setOnClickListener(new OnClickListener() {
+
+					@Override
+					public void onClick(View v) {
+						AlertDialog dialog = UIHelper
+								.getVerifyFingerprintDialog(
+										(ConversationActivity) getActivity(),
+										conversation, fingerprintWarning);
+						dialog.show();
+					}
+				});
+			} else {
+				fingerprintWarning.setVisibility(View.GONE);
+			}
 		} else {
 			fingerprintWarning.setVisibility(View.GONE);
 		}
@@ -300,11 +331,11 @@ public class ConversationFragment extends Fragment {
 
 	protected void sendPlainTextMessage(Message message) {
 		ConversationActivity activity = (ConversationActivity) getActivity();
-		activity.xmppConnectionService.sendMessage(conversation.getAccount(), message,
-				null);
+		activity.xmppConnectionService.sendMessage(conversation.getAccount(),
+				message, null);
 		chatMsg.setText("");
 	}
-	
+
 	protected void sendOtrMessage(final Message message) {
 		ConversationActivity activity = (ConversationActivity) getActivity();
 		final XmppConnectionService xmppService = activity.xmppConnectionService;
@@ -313,9 +344,13 @@ public class ConversationFragment extends Fragment {
 					conversation.getAccount(), message, null);
 			chatMsg.setText("");
 		} else {
-			Hashtable<String, Integer> presences = conversation
-					.getContact().getPresences();
-			if (presences.size() == 0) {
+			Hashtable<String, Integer> presences;
+			if (conversation.getContact() != null) {
+				presences = conversation.getContact().getPresences();
+			} else {
+				presences = null;
+			}
+			if ((presences != null) && (presences.size() == 0)) {
 				AlertDialog.Builder builder = new AlertDialog.Builder(
 						getActivity());
 				builder.setTitle("Contact is offline");
@@ -330,16 +365,15 @@ public class ConversationFragment extends Fragment {
 								conversation.nextMessageEncryption = Message.ENCRYPTION_NONE;
 								message.setEncryption(Message.ENCRYPTION_NONE);
 								xmppService.sendMessage(
-										conversation.getAccount(),
-										message, null);
+										conversation.getAccount(), message,
+										null);
 								chatMsg.setText("");
 							}
 						});
 				builder.setNegativeButton("Cancel", null);
 				builder.create().show();
 			} else if (presences.size() == 1) {
-				xmppService.sendMessage(conversation.getAccount(),
-						message,
+				xmppService.sendMessage(conversation.getAccount(), message,
 						(String) presences.keySet().toArray()[0]);
 				chatMsg.setText("");
 			} else {
@@ -348,16 +382,51 @@ public class ConversationFragment extends Fragment {
 				builder.setTitle("Choose Presence");
 				final String[] presencesArray = new String[presences.size()];
 				presences.keySet().toArray(presencesArray);
-				builder.setItems(presencesArray, new DialogInterface.OnClickListener() {
-					
-					@Override
-					public void onClick(DialogInterface dialog, int which) {
-						xmppService.sendMessage(conversation.getAccount(), message, presencesArray[which]);
-						chatMsg.setText("");
-					}
-				});
+				builder.setItems(presencesArray,
+						new DialogInterface.OnClickListener() {
+
+							@Override
+							public void onClick(DialogInterface dialog,
+									int which) {
+								xmppService.sendMessage(
+										conversation.getAccount(), message,
+										presencesArray[which]);
+								chatMsg.setText("");
+							}
+						});
 				builder.create().show();
 			}
 		}
 	}
+
+	private static class ViewHolder {
+
+		protected TextView time;
+		protected TextView messageBody;
+		protected ImageView imageView;
+
+	}
+	
+	private class BitmapCache {
+		private HashMap<String, Bitmap> bitmaps = new HashMap<String, Bitmap>();
+		public Bitmap get(String name, Uri uri) {
+			if (bitmaps.containsKey(name)) {
+				return bitmaps.get(name);
+			} else {
+				Bitmap bm;
+				if (uri!=null) {
+					try {
+						bm = BitmapFactory.decodeStream(getActivity()
+								.getContentResolver().openInputStream(uri));
+					} catch (FileNotFoundException e) {
+						bm = UIHelper.getUnknownContactPicture(name, 200);
+					}
+				} else {
+					bm = UIHelper.getUnknownContactPicture(name, 200);
+				}
+				bitmaps.put(name, bm);
+				return bm;
+			}
+		}
+	}
 }

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

@@ -0,0 +1,119 @@
+package de.gultsch.chat.utils;
+
+import java.util.List;
+
+import net.java.otr4j.session.Session;
+import net.java.otr4j.session.SessionStatus;
+import android.util.Log;
+import de.gultsch.chat.entities.Account;
+import de.gultsch.chat.entities.Conversation;
+import de.gultsch.chat.entities.Message;
+import de.gultsch.chat.services.XmppConnectionService;
+import de.gultsch.chat.xml.Element;
+import de.gultsch.chat.xmpp.MessagePacket;
+
+public class MessageParser {
+	
+	protected static final String LOGTAG = "xmppService";
+	
+	public static Message parsePlainTextChat(MessagePacket packet, Account account, XmppConnectionService service) {
+		String[] fromParts = packet.getFrom().split("/");
+		Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false);
+		String body = packet.getBody();
+		return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_NONE, Message.STATUS_RECIEVED);
+	}
+	
+	public static Message parseOtrChat(MessagePacket packet, Account account, XmppConnectionService service) {
+		String[] fromParts = packet.getFrom().split("/");
+		Conversation conversation = service.findOrCreateConversation(account, fromParts[0],false);
+		String body = packet.getBody();
+		if (!conversation.hasValidOtrSession()) {
+			conversation.startOtrSession(service.getApplicationContext(), fromParts[1]);
+		}
+		try {
+			Session otrSession = conversation.getOtrSession();
+			SessionStatus before = otrSession
+					.getSessionStatus();
+			body = otrSession.transformReceiving(body);
+			SessionStatus after = otrSession.getSessionStatus();
+			if ((before != after)
+					&& (after == SessionStatus.ENCRYPTED)) {
+				Log.d(LOGTAG, "otr session etablished");
+				List<Message> messages = conversation
+						.getMessages();
+				for (int i = 0; i < messages.size(); ++i) {
+					Message msg = messages.get(i);
+					if ((msg.getStatus() == Message.STATUS_UNSEND)
+							&& (msg.getEncryption() == Message.ENCRYPTION_OTR)) {
+						MessagePacket outPacket = service.prepareMessagePacket(
+								account, msg, otrSession);
+						msg.setStatus(Message.STATUS_SEND);
+						service.databaseBackend.updateMessage(msg);
+						account.getXmppConnection()
+								.sendMessagePacket(outPacket);
+					}
+				}
+				if (service.convChangedListener!=null) {
+					service.convChangedListener.onConversationListChanged();
+				}
+			} else if ((before != after) && (after == SessionStatus.FINISHED)) {
+				conversation.resetOtrSession();
+				Log.d(LOGTAG,"otr session stoped");
+			}
+		} catch (Exception e) {
+			Log.d(LOGTAG, "error receiving otr. resetting");
+			conversation.resetOtrSession();
+			return null;
+		}
+		if (body == null) {
+			return null;
+		}
+		return new Message(conversation, packet.getFrom(), body, Message.ENCRYPTION_OTR,Message.STATUS_RECIEVED);
+	}
+	
+	public static Message parseGroupchat(MessagePacket packet, Account account, XmppConnectionService service) {
+		int status;
+		String[] fromParts = packet.getFrom().split("/");
+		Conversation conversation = service.findOrCreateConversation(account, fromParts[0],true);
+		if ((fromParts.length == 1) || (packet.hasChild("subject"))) {
+			return null;
+		}
+		String counterPart = fromParts[1];
+		if (counterPart.equals(account.getUsername())) {
+			status = Message.STATUS_SEND;
+		} else {
+			status = Message.STATUS_RECIEVED;
+		}
+		return new Message(conversation, counterPart, packet.getBody(), Message.ENCRYPTION_NONE, status);
+	}
+
+	public static Message parseCarbonMessage(MessagePacket packet,
+			Account account, XmppConnectionService service) {
+		// TODO Auto-generated method stub
+		int status;
+		String fullJid;
+		Element forwarded;
+		if (packet.hasChild("received")) {
+			forwarded = packet.findChild("received").findChild(
+					"forwarded");
+			status = Message.STATUS_RECIEVED;
+		} else if (packet.hasChild("sent")) {
+			forwarded = packet.findChild("sent").findChild(
+					"forwarded");
+			status = Message.STATUS_SEND;
+		} else {
+			return null;
+		}
+		Element message = forwarded.findChild("message");
+		if ((message == null) || (!message.hasChild("body")))
+			return null; // either malformed or boring
+		if (status == Message.STATUS_RECIEVED) {
+			fullJid = message.getAttribute("from");
+		} else {
+			fullJid = message.getAttribute("to");
+		}
+		String[] parts = fullJid.split("/");
+		Conversation conversation = service.findOrCreateConversation(account, parts[0],false);
+		return new Message(conversation,fullJid, message.findChild("body").getContent(), Message.ENCRYPTION_NONE,status);
+	}
+}

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

@@ -1,6 +1,5 @@
 package de.gultsch.chat.utils;
 
-import java.util.ArrayList;
 import java.util.Hashtable;
 
 import android.app.Activity;
@@ -11,14 +10,19 @@ import android.content.Loader.OnLoadCompleteListener;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Looper;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Profile;
 
 public class PhoneHelper {
 	
 	public static void loadPhoneContacts(Context context, final OnPhoneContactsLoadedListener listener) {
+		if (Looper.myLooper()==null) {
+			Looper.prepare();
+		}
+		final Looper mLooper = Looper.myLooper();
 		final Hashtable<String, Bundle> phoneContacts = new Hashtable<String, Bundle>();
-
+		
 		final String[] PROJECTION = new String[] {
 				ContactsContract.Data._ID,
 				ContactsContract.Data.DISPLAY_NAME,
@@ -31,7 +35,7 @@ public class PhoneHelper {
 				+ "\") AND (" + ContactsContract.CommonDataKinds.Im.PROTOCOL
 				+ "=\"" + ContactsContract.CommonDataKinds.Im.PROTOCOL_JABBER
 				+ "\")";
-
+		
 		CursorLoader mCursorLoader = new CursorLoader(context,
 				ContactsContract.Data.CONTENT_URI, PROJECTION, SELECTION, null,
 				null);
@@ -61,6 +65,7 @@ public class PhoneHelper {
 				if (listener!=null) {
 					listener.onPhoneContactsLoaded(phoneContacts);
 				}
+				mLooper.quit();
 			}
 		});
 		mCursorLoader.startLoading();

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

@@ -59,6 +59,10 @@ public class XmlReader {
 					for(int i = 0; i < parser.getAttributeCount(); ++i) {
 						tag.setAttribute(parser.getAttributeName(i), parser.getAttributeValue(i));
 					}
+					String xmlns = 	parser.getNamespace();
+					if (xmlns!=null) {
+						tag.setAttribute("xmlns",xmlns);
+					}
 					return tag;
 				} else if (parser.getEventType() == XmlPullParser.END_TAG) {
 					Tag tag = Tag.end(parser.getName());