XmppConnectionService.java

  1package de.gultsch.chat.services;
  2
  3import java.text.ParseException;
  4import java.text.SimpleDateFormat;
  5import java.util.ArrayList;
  6import java.util.Date;
  7import java.util.Hashtable;
  8import java.util.List;
  9
 10import de.gultsch.chat.entities.Account;
 11import de.gultsch.chat.entities.Contact;
 12import de.gultsch.chat.entities.Conversation;
 13import de.gultsch.chat.entities.Message;
 14import de.gultsch.chat.entities.Presences;
 15import de.gultsch.chat.persistance.DatabaseBackend;
 16import de.gultsch.chat.ui.OnAccountListChangedListener;
 17import de.gultsch.chat.ui.OnConversationListChangedListener;
 18import de.gultsch.chat.ui.OnRosterFetchedListener;
 19import de.gultsch.chat.utils.OnPhoneContactsLoadedListener;
 20import de.gultsch.chat.utils.PhoneHelper;
 21import de.gultsch.chat.utils.UIHelper;
 22import de.gultsch.chat.xml.Element;
 23import de.gultsch.chat.xmpp.IqPacket;
 24import de.gultsch.chat.xmpp.MessagePacket;
 25import de.gultsch.chat.xmpp.OnIqPacketReceived;
 26import de.gultsch.chat.xmpp.OnMessagePacketReceived;
 27import de.gultsch.chat.xmpp.OnPresencePacketReceived;
 28import de.gultsch.chat.xmpp.OnStatusChanged;
 29import de.gultsch.chat.xmpp.PresencePacket;
 30import de.gultsch.chat.xmpp.XmppConnection;
 31import android.app.NotificationManager;
 32import android.app.Service;
 33import android.content.Context;
 34import android.content.Intent;
 35import android.database.ContentObserver;
 36import android.os.Binder;
 37import android.os.Bundle;
 38import android.os.IBinder;
 39import android.os.PowerManager;
 40import android.provider.ContactsContract;
 41import android.util.Log;
 42
 43public class XmppConnectionService extends Service {
 44
 45	protected static final String LOGTAG = "xmppService";
 46	protected DatabaseBackend databaseBackend;
 47
 48	public long startDate;
 49
 50	private List<Account> accounts;
 51	private List<Conversation> conversations = null;
 52
 53	private Hashtable<Account, XmppConnection> connections = new Hashtable<Account, XmppConnection>();
 54
 55	private OnConversationListChangedListener convChangedListener = null;
 56	private OnAccountListChangedListener accountChangedListener = null;
 57
 58	private ContentObserver contactObserver = new ContentObserver(null) {
 59		@Override
 60		public void onChange(boolean selfChange) {
 61			super.onChange(selfChange);
 62			Log.d(LOGTAG, "contact list has changed");
 63			mergePhoneContactsWithRoster();
 64		}
 65	};
 66
 67	private final IBinder mBinder = new XmppConnectionBinder();
 68	private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
 69
 70		@Override
 71		public void onMessagePacketReceived(Account account,
 72				MessagePacket packet) {
 73			if ((packet.getType() == MessagePacket.TYPE_CHAT)
 74					|| (packet.getType() == MessagePacket.TYPE_GROUPCHAT)) {
 75				boolean notify = true;
 76				int status = Message.STATUS_RECIEVED;
 77				String body;
 78				String fullJid;
 79				if (!packet.hasChild("body")) {
 80					Element forwarded;
 81					if (packet.hasChild("received")) {
 82						forwarded = packet.findChild("received").findChild(
 83								"forwarded");
 84					} else if (packet.hasChild("sent")) {
 85						forwarded = packet.findChild("sent").findChild(
 86								"forwarded");
 87						status = Message.STATUS_SEND;
 88						notify = false;
 89					} else {
 90						return; // massage has no body and is not carbon. just
 91						// skip
 92					}
 93					if (forwarded != null) {
 94						Element message = forwarded.findChild("message");
 95						if ((message == null) || (!message.hasChild("body")))
 96							return; // either malformed or boring
 97						if (status == Message.STATUS_RECIEVED) {
 98							fullJid = message.getAttribute("from");
 99						} else {
100							fullJid = message.getAttribute("to");
101						}
102						body = message.findChild("body").getContent();
103					} else {
104						return; // packet malformed. has no forwarded element
105					}
106				} else {
107					fullJid = packet.getFrom();
108					body = packet.getBody();
109				}
110				Conversation conversation = null;
111				String[] fromParts = fullJid.split("/");
112				String jid = fromParts[0];
113				boolean muc = (packet.getType() == MessagePacket.TYPE_GROUPCHAT);
114				String counterPart = null;
115				conversation = findOrCreateConversation(account, jid, muc);
116				if (muc) {
117					if ((fromParts.length == 1) || (packet.hasChild("subject"))) {
118						return;
119					}
120					counterPart = fromParts[1];
121					if (counterPart.equals(account.getUsername())) {
122						status = Message.STATUS_SEND;
123						notify = false;
124					}
125				} else {
126					counterPart = fullJid;
127				}
128				Message message = new Message(conversation, counterPart, body,
129						Message.ENCRYPTION_NONE, status);
130				if (packet.hasChild("delay")) {
131					try {
132						String stamp = packet.findChild("delay").getAttribute(
133								"stamp");
134						stamp = stamp.replace("Z", "+0000");
135						Date date = new SimpleDateFormat(
136								"yyyy-MM-dd'T'HH:mm:ssZ").parse(stamp);
137						message.setTime(date.getTime());
138					} catch (ParseException e) {
139						Log.d(LOGTAG,
140								"error trying to parse date" + e.getMessage());
141					}
142				}
143				if (notify) {
144					message.markUnread();
145				}
146				conversation.getMessages().add(message);
147				databaseBackend.createMessage(message);
148				if (convChangedListener != null) {
149					convChangedListener.onConversationListChanged();
150				} else {
151					if (notify) {
152						NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
153						mNotificationManager.notify(2342, UIHelper
154								.getUnreadMessageNotification(
155										getApplicationContext(), conversation));
156					}
157				}
158			}
159		}
160	};
161	private OnStatusChanged statusListener = new OnStatusChanged() {
162
163		@Override
164		public void onStatusChanged(Account account) {
165			if (accountChangedListener != null) {
166				accountChangedListener.onAccountListChangedListener();
167			}
168			if (account.getStatus() == Account.STATUS_ONLINE) {
169				databaseBackend.clearPresences(account);
170				connectMultiModeConversations(account);
171				List<Conversation> conversations = getConversations();
172 				for(int i = 0; i < conversations.size(); ++i) {
173 					if (conversations.get(i).getAccount()==account) {
174 						sendUnsendMessages(conversations.get(i));
175 					}
176 				}
177 				if (convChangedListener!=null) {
178 					convChangedListener.onConversationListChanged();
179 				}
180			}
181		}
182	};
183
184	private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
185
186		@Override
187		public void onPresencePacketReceived(Account account,
188				PresencePacket packet) {
189			String[] fromParts = packet.getAttribute("from").split("/");
190			Contact contact = findContact(account, fromParts[0]);
191			if (contact == null) {
192				// most likely muc, self or roster not synced
193				// Log.d(LOGTAG,"got presence for non contact "+packet.toString());
194				return;
195			}
196			String type = packet.getAttribute("type");
197			if (type == null) {
198				Element show = packet.findChild("show");
199				if (show == null) {
200					contact.updatePresence(fromParts[1], Presences.ONLINE);
201				} else if (show.getContent().equals("away")) {
202					contact.updatePresence(fromParts[1], Presences.AWAY);
203				} else if (show.getContent().equals("xa")) {
204					contact.updatePresence(fromParts[1], Presences.XA);
205				} else if (show.getContent().equals("chat")) {
206					contact.updatePresence(fromParts[1], Presences.CHAT);
207				} else if (show.getContent().equals("dnd")) {
208					contact.updatePresence(fromParts[1], Presences.DND);
209				}
210				databaseBackend.updateContact(contact);
211			} else if (type.equals("unavailable")) {
212				if (fromParts.length != 2) {
213					// Log.d(LOGTAG,"received presence with no resource "+packet.toString());
214				} else {
215					contact.removePresence(fromParts[1]);
216					databaseBackend.updateContact(contact);
217				}
218			}
219		}
220	};
221
222	public class XmppConnectionBinder extends Binder {
223		public XmppConnectionService getService() {
224			return XmppConnectionService.this;
225		}
226	}
227
228	@Override
229	public int onStartCommand(Intent intent, int flags, int startId) {
230		for (Account account : accounts) {
231			if (!connections.containsKey(account)) {
232				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
233					this.connections.put(account,
234							this.createConnection(account));
235				} else {
236					Log.d(LOGTAG, account.getJid()
237							+ ": not starting because it's disabled");
238				}
239			}
240		}
241		return START_STICKY;
242	}
243
244	@Override
245	public void onCreate() {
246		databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
247		this.accounts = databaseBackend.getAccounts();
248
249		getContentResolver().registerContentObserver(
250				ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
251	}
252
253	public XmppConnection createConnection(Account account) {
254		PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
255		XmppConnection connection = new XmppConnection(account, pm);
256		connection.setOnMessagePacketReceivedListener(this.messageListener);
257		connection.setOnStatusChangedListener(this.statusListener);
258		connection.setOnPresencePacketReceivedListener(this.presenceListener);
259		Thread thread = new Thread(connection);
260		thread.start();
261		return connection;
262	}
263
264	public void sendMessage(Account account, Message message) {
265
266		if (account.getStatus() == Account.STATUS_ONLINE) {
267			MessagePacket packet = prepareMessagePacket(account, message);
268			connections.get(account).sendMessagePacket(packet);
269			if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
270				message.setStatus(Message.STATUS_SEND);
271				if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
272					databaseBackend.createMessage(message);
273					message.getConversation().getMessages().add(message);
274					if (convChangedListener!=null) {
275						convChangedListener.onConversationListChanged();
276					}
277				}
278			}
279		} else {
280			message.getConversation().getMessages().add(message);
281			databaseBackend.createMessage(message);
282			if (convChangedListener!=null) {
283				convChangedListener.onConversationListChanged();
284			}
285		}
286		
287	}
288
289	private void sendUnsendMessages(Conversation conversation) {
290		for (int i = 0; i < conversation.getMessages().size(); ++i) {
291			if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) {
292				Message message = conversation.getMessages()
293						.get(i);
294				MessagePacket packet = prepareMessagePacket(
295						conversation.getAccount(),message);
296				connections.get(conversation.getAccount()).sendMessagePacket(
297						packet);
298				message.setStatus(Message.STATUS_SEND);
299				if (conversation.getMode() == Conversation.MODE_SINGLE) {
300					databaseBackend.updateMessage(message);
301				} else {
302					databaseBackend.deleteMessage(message);
303					conversation.getMessages().remove(i);
304					i--;
305				}
306			}
307		}
308	}
309
310	private MessagePacket prepareMessagePacket(Account account, Message message) {
311		MessagePacket packet = new MessagePacket();
312		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
313			packet.setType(MessagePacket.TYPE_CHAT);
314		} else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
315			packet.setType(MessagePacket.TYPE_GROUPCHAT);
316		}
317		packet.setTo(message.getCounterpart());
318		packet.setFrom(account.getJid());
319		packet.setBody(message.getBody());
320		return packet;
321	}
322
323	public void getRoster(Account account,
324			final OnRosterFetchedListener listener) {
325		List<Contact> contacts = databaseBackend.getContacts(account);
326		for (int i = 0; i < contacts.size(); ++i) {
327			contacts.get(i).setAccount(account);
328		}
329		if (listener != null) {
330			listener.onRosterFetched(contacts);
331		}
332	}
333
334	public void updateRoster(final Account account,
335			final OnRosterFetchedListener listener) {
336
337		PhoneHelper.loadPhoneContacts(this,
338				new OnPhoneContactsLoadedListener() {
339
340					@Override
341					public void onPhoneContactsLoaded(
342							final Hashtable<String, Bundle> phoneContacts) {
343						IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
344						Element query = new Element("query");
345						query.setAttribute("xmlns", "jabber:iq:roster");
346						query.setAttribute("ver", "");
347						iqPacket.addChild(query);
348						connections.get(account).sendIqPacket(iqPacket,
349								new OnIqPacketReceived() {
350
351									@Override
352									public void onIqPacketReceived(
353											Account account, IqPacket packet) {
354										List<Contact> contacts = new ArrayList<Contact>();
355										Element roster = packet
356												.findChild("query");
357										if (roster != null) {
358											for (Element item : roster
359													.getChildren()) {
360												Contact contact;
361												String name = item
362														.getAttribute("name");
363												String jid = item
364														.getAttribute("jid");
365												if (phoneContacts
366														.containsKey(jid)) {
367													Bundle phoneContact = phoneContacts
368															.get(jid);
369													String systemAccount = phoneContact
370															.getInt("phoneid")
371															+ "#"
372															+ phoneContact
373																	.getString("lookup");
374													contact = new Contact(
375															account,
376															phoneContact
377																	.getString("displayname"),
378															jid,
379															phoneContact
380																	.getString("photouri"));
381													contact.setSystemAccount(systemAccount);
382												} else {
383													if (name == null) {
384														name = jid.split("@")[0];
385													}
386													contact = new Contact(
387															account, name, jid,
388															null);
389
390												}
391												contact.setAccount(account);
392												contact.setSubscription(item
393														.getAttribute("subscription"));
394												contacts.add(contact);
395											}
396											databaseBackend
397													.mergeContacts(contacts);
398											if (listener != null) {
399												listener.onRosterFetched(contacts);
400											}
401										}
402									}
403								});
404
405					}
406				});
407	}
408
409	public void mergePhoneContactsWithRoster() {
410		PhoneHelper.loadPhoneContacts(this,
411				new OnPhoneContactsLoadedListener() {
412					@Override
413					public void onPhoneContactsLoaded(
414							Hashtable<String, Bundle> phoneContacts) {
415						List<Contact> contacts = databaseBackend
416								.getContacts(null);
417						for (int i = 0; i < contacts.size(); ++i) {
418							Contact contact = contacts.get(i);
419							if (phoneContacts.containsKey(contact.getJid())) {
420								Bundle phoneContact = phoneContacts.get(contact
421										.getJid());
422								String systemAccount = phoneContact
423										.getInt("phoneid")
424										+ "#"
425										+ phoneContact.getString("lookup");
426								contact.setSystemAccount(systemAccount);
427								contact.setPhotoUri(phoneContact
428										.getString("photouri"));
429								contact.setDisplayName(phoneContact
430										.getString("displayname"));
431								databaseBackend.updateContact(contact);
432							} else {
433								if ((contact.getSystemAccount() != null)
434										|| (contact.getProfilePhoto() != null)) {
435									contact.setSystemAccount(null);
436									contact.setPhotoUri(null);
437									databaseBackend.updateContact(contact);
438								}
439							}
440						}
441					}
442				});
443	}
444
445	public void addConversation(Conversation conversation) {
446		databaseBackend.createConversation(conversation);
447	}
448
449	public List<Conversation> getConversations() {
450		if (this.conversations == null) {
451			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
452			for (Account account : this.accounts) {
453				accountLookupTable.put(account.getUuid(), account);
454			}
455			this.conversations = databaseBackend
456					.getConversations(Conversation.STATUS_AVAILABLE);
457			for (Conversation conv : this.conversations) {
458				Account account = accountLookupTable.get(conv.getAccountUuid());
459				conv.setAccount(account);
460				conv.setContact(findContact(account, conv.getContactJid()));
461				conv.setMessages(databaseBackend.getMessages(conv, 50));
462			}
463		}
464		return this.conversations;
465	}
466
467	public List<Account> getAccounts() {
468		return this.accounts;
469	}
470
471	public Contact findContact(Account account, String jid) {
472		return databaseBackend.findContact(account, jid);
473	}
474
475	public Conversation findOrCreateConversation(Account account, String jid,
476			boolean muc) {
477		for (Conversation conv : this.getConversations()) {
478			if ((conv.getAccount().equals(account))
479					&& (conv.getContactJid().equals(jid))) {
480				return conv;
481			}
482		}
483		Conversation conversation = databaseBackend.findConversation(account,
484				jid);
485		if (conversation != null) {
486			conversation.setStatus(Conversation.STATUS_AVAILABLE);
487			conversation.setAccount(account);
488			if (muc) {
489				conversation.setMode(Conversation.MODE_MULTI);
490				if (account.getStatus() == Account.STATUS_ONLINE) {
491					joinMuc(account, conversation);
492				}
493			} else {
494				conversation.setMode(Conversation.MODE_SINGLE);
495			}
496			this.databaseBackend.updateConversation(conversation);
497		} else {
498			String conversationName;
499			Contact contact = findContact(account, jid);
500			if (contact != null) {
501				conversationName = contact.getDisplayName();
502			} else {
503				conversationName = jid.split("@")[0];
504			}
505			if (muc) {
506				conversation = new Conversation(conversationName, account, jid,
507						Conversation.MODE_MULTI);
508				if (account.getStatus() == Account.STATUS_ONLINE) {
509					joinMuc(account, conversation);
510				}
511			} else {
512				conversation = new Conversation(conversationName, account, jid,
513						Conversation.MODE_SINGLE);
514			}
515			conversation.setContact(contact);
516			this.databaseBackend.createConversation(conversation);
517		}
518		this.conversations.add(conversation);
519		if (this.convChangedListener != null) {
520			this.convChangedListener.onConversationListChanged();
521		}
522		return conversation;
523	}
524
525	public void archiveConversation(Conversation conversation) {
526		this.databaseBackend.updateConversation(conversation);
527		this.conversations.remove(conversation);
528		if (this.convChangedListener != null) {
529			this.convChangedListener.onConversationListChanged();
530		}
531	}
532
533	public int getConversationCount() {
534		return this.databaseBackend.getConversationCount();
535	}
536
537	public void createAccount(Account account) {
538		databaseBackend.createAccount(account);
539		this.accounts.add(account);
540		this.connections.put(account, this.createConnection(account));
541		if (accountChangedListener != null)
542			accountChangedListener.onAccountListChangedListener();
543	}
544
545	public void updateAccount(Account account) {
546		databaseBackend.updateAccount(account);
547		XmppConnection connection = this.connections.get(account);
548		if (connection != null) {
549			connection.disconnect();
550			this.connections.remove(account);
551		}
552		if (!account.isOptionSet(Account.OPTION_DISABLED)) {
553			this.connections.put(account, this.createConnection(account));
554		} else {
555			Log.d(LOGTAG, account.getJid()
556					+ ": not starting because it's disabled");
557		}
558		if (accountChangedListener != null)
559			accountChangedListener.onAccountListChangedListener();
560	}
561
562	public void deleteAccount(Account account) {
563		Log.d(LOGTAG, "called delete account");
564		if (this.connections.containsKey(account)) {
565			Log.d(LOGTAG, "found connection. disconnecting");
566			this.connections.get(account).disconnect();
567			this.connections.remove(account);
568		}
569		databaseBackend.deleteAccount(account);
570		this.accounts.remove(account);
571		if (accountChangedListener != null)
572			accountChangedListener.onAccountListChangedListener();
573	}
574
575	public void setOnConversationListChangedListener(
576			OnConversationListChangedListener listener) {
577		this.convChangedListener = listener;
578	}
579
580	public void removeOnConversationListChangedListener() {
581		this.convChangedListener = null;
582	}
583
584	public void setOnAccountListChangedListener(
585			OnAccountListChangedListener listener) {
586		this.accountChangedListener = listener;
587	}
588
589	public void removeOnAccountListChangedListener() {
590		this.accountChangedListener = null;
591	}
592
593	public void connectMultiModeConversations(Account account) {
594		List<Conversation> conversations = getConversations();
595		for (int i = 0; i < conversations.size(); i++) {
596			Conversation conversation = conversations.get(i);
597			if ((conversation.getMode() == Conversation.MODE_MULTI)
598					&& (conversation.getAccount() == account)) {
599				joinMuc(account, conversation);
600			}
601		}
602	}
603
604	public void joinMuc(Account account, Conversation conversation) {
605		String muc = conversation.getContactJid();
606		PresencePacket packet = new PresencePacket();
607		packet.setAttribute("to", muc + "/" + account.getUsername());
608		Element x = new Element("x");
609		x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
610		if (conversation.getMessages().size() != 0) {
611			Element history = new Element("history");
612			history.setAttribute(
613					"seconds",
614					(System.currentTimeMillis() - conversation
615							.getLatestMessageDate()) / 1000 + "");
616			x.addChild(history);
617		}
618		packet.addChild(x);
619		connections.get(conversation.getAccount()).sendPresencePacket(packet);
620	}
621
622	public void disconnectMultiModeConversations() {
623
624	}
625
626	@Override
627	public IBinder onBind(Intent intent) {
628		return mBinder;
629	}
630}