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("stamp");
133						stamp = stamp.replace("Z","+0000");
134						Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").parse(stamp);
135						message.setTime(date.getTime());
136					} catch (ParseException e) {
137						Log.d(LOGTAG,"error trying to parse date"+e.getMessage());
138					}
139				}
140				if(notify) {
141					message.markUnread();
142				}
143				conversation.getMessages().add(message);
144				databaseBackend.createMessage(message);
145				if (convChangedListener != null) {
146					convChangedListener.onConversationListChanged();
147				} else {
148					if (notify) {
149						NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
150						mNotificationManager.notify(2342, UIHelper
151								.getUnreadMessageNotification(
152										getApplicationContext(), conversation));
153					}
154				}
155			}
156		}
157	};
158	private OnStatusChanged statusListener = new OnStatusChanged() {
159
160		@Override
161		public void onStatusChanged(Account account) {
162			if (accountChangedListener != null) {
163				accountChangedListener.onAccountListChangedListener();
164			}
165			if (account.getStatus() == Account.STATUS_ONLINE) {
166				databaseBackend.clearPresences(account);
167				connectMultiModeConversations(account);
168			}
169		}
170	};
171
172	private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
173
174		@Override
175		public void onPresencePacketReceived(Account account,
176				PresencePacket packet) {
177			String[] fromParts = packet.getAttribute("from").split("/");
178			Contact contact = findContact(account, fromParts[0]);
179			if (contact == null) {
180				// most likely muc, self or roster not synced
181				// Log.d(LOGTAG,"got presence for non contact "+packet.toString());
182				return;
183			}
184			String type = packet.getAttribute("type");
185			if (type == null) {
186				Element show = packet.findChild("show");
187				if (show == null) {
188					contact.updatePresence(fromParts[1], Presences.ONLINE);
189				} else if (show.getContent().equals("away")) {
190					contact.updatePresence(fromParts[1], Presences.AWAY);
191				} else if (show.getContent().equals("xa")) {
192					contact.updatePresence(fromParts[1], Presences.XA);
193				} else if (show.getContent().equals("chat")) {
194					contact.updatePresence(fromParts[1], Presences.CHAT);
195				} else if (show.getContent().equals("dnd")) {
196					contact.updatePresence(fromParts[1], Presences.DND);
197				}
198				databaseBackend.updateContact(contact);
199			} else if (type.equals("unavailable")) {
200				if (fromParts.length != 2) {
201					// Log.d(LOGTAG,"received presence with no resource "+packet.toString());
202				} else {
203					contact.removePresence(fromParts[1]);
204					databaseBackend.updateContact(contact);
205				}
206			}
207		}
208	};
209
210	public class XmppConnectionBinder extends Binder {
211		public XmppConnectionService getService() {
212			return XmppConnectionService.this;
213		}
214	}
215
216	@Override
217	public int onStartCommand(Intent intent, int flags, int startId) {
218		for (Account account : accounts) {
219			if (!connections.containsKey(account)) {
220				if (!account.isOptionSet(Account.OPTION_DISABLED)) {
221					this.connections.put(account,
222							this.createConnection(account));
223				} else {
224					Log.d(LOGTAG, account.getJid()
225							+ ": not starting because it's disabled");
226				}
227			}
228		}
229		return START_STICKY;
230	}
231
232	@Override
233	public void onCreate() {
234		databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
235		this.accounts = databaseBackend.getAccounts();
236		
237		getContentResolver()
238        .registerContentObserver(
239                ContactsContract.Contacts.CONTENT_URI, true,contactObserver);   
240	}
241
242	public XmppConnection createConnection(Account account) {
243		PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
244		XmppConnection connection = new XmppConnection(account, pm);
245		connection.setOnMessagePacketReceivedListener(this.messageListener);
246		connection.setOnStatusChangedListener(this.statusListener);
247		connection.setOnPresencePacketReceivedListener(this.presenceListener);
248		Thread thread = new Thread(connection);
249		thread.start();
250		return connection;
251	}
252
253	public void sendMessage(final Account account, final Message message) {
254		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
255			databaseBackend.createMessage(message);
256		}
257		MessagePacket packet = new MessagePacket();
258		if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
259			packet.setType(MessagePacket.TYPE_CHAT);
260		} else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
261			packet.setType(MessagePacket.TYPE_GROUPCHAT);
262		}
263		packet.setTo(message.getCounterpart());
264		packet.setFrom(account.getJid());
265		packet.setBody(message.getBody());
266		if (account.getStatus() == Account.STATUS_ONLINE) {
267			connections.get(account).sendMessagePacket(packet);
268			if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
269				message.setStatus(Message.STATUS_SEND);
270				databaseBackend.updateMessage(message);
271			}
272		}
273	}
274
275	public void getRoster(Account account,
276			final OnRosterFetchedListener listener) {
277		List<Contact> contacts = databaseBackend.getContacts(account);
278		for (int i = 0; i < contacts.size(); ++i) {
279			contacts.get(i).setAccount(account);
280		}
281		if (listener != null) {
282			listener.onRosterFetched(contacts);
283		}
284	}
285
286	public void updateRoster(final Account account,
287			final OnRosterFetchedListener listener) {
288
289		PhoneHelper.loadPhoneContacts(this,
290				new OnPhoneContactsLoadedListener() {
291
292					@Override
293					public void onPhoneContactsLoaded(
294							final Hashtable<String, Bundle> phoneContacts) {
295						IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
296						Element query = new Element("query");
297						query.setAttribute("xmlns", "jabber:iq:roster");
298						query.setAttribute("ver", "");
299						iqPacket.addChild(query);
300						connections.get(account).sendIqPacket(iqPacket,
301								new OnIqPacketReceived() {
302
303									@Override
304									public void onIqPacketReceived(
305											Account account, IqPacket packet) {
306										List<Contact> contacts = new ArrayList<Contact>();
307										Element roster = packet
308												.findChild("query");
309										if (roster != null) {
310											for (Element item : roster
311													.getChildren()) {
312												Contact contact;
313												String name = item
314														.getAttribute("name");
315												String jid = item
316														.getAttribute("jid");
317												if (phoneContacts
318														.containsKey(jid)) {
319													Bundle phoneContact = phoneContacts
320															.get(jid);
321													String systemAccount = phoneContact
322															.getInt("phoneid")
323															+ "#"
324															+ phoneContact
325																	.getString("lookup");
326													contact = new Contact(
327															account,
328															phoneContact
329																	.getString("displayname"),
330															jid,
331															phoneContact
332																	.getString("photouri"));
333													contact.setSystemAccount(systemAccount);
334												} else {
335													if (name == null) {
336														name = jid.split("@")[0];
337													}
338													contact = new Contact(
339															account, name, jid,
340															null);
341
342												}
343												contact.setAccount(account);
344												contact.setSubscription(item
345														.getAttribute("subscription"));
346												contacts.add(contact);
347											}
348											databaseBackend
349													.mergeContacts(contacts);
350											if (listener != null) {
351												listener.onRosterFetched(contacts);
352											}
353										}
354									}
355								});
356
357					}
358				});
359	}
360
361	public void mergePhoneContactsWithRoster() {
362		PhoneHelper.loadPhoneContacts(this,
363				new OnPhoneContactsLoadedListener() {
364					@Override
365					public void onPhoneContactsLoaded(
366							Hashtable<String, Bundle> phoneContacts) {
367						List<Contact> contacts = databaseBackend
368								.getContacts(null);
369						for (int i = 0; i < contacts.size(); ++i) {
370							Contact contact = contacts.get(i);
371							if (phoneContacts.containsKey(contact.getJid())) {
372								Bundle phoneContact = phoneContacts.get(contact
373										.getJid());
374								String systemAccount = phoneContact
375										.getInt("phoneid")
376										+ "#"
377										+ phoneContact.getString("lookup");
378								contact.setSystemAccount(systemAccount);
379								contact.setPhotoUri(phoneContact
380										.getString("photouri"));
381								contact.setDisplayName(phoneContact
382										.getString("displayname"));
383								databaseBackend.updateContact(contact);
384							} else {
385								if ((contact.getSystemAccount() != null)
386										|| (contact.getProfilePhoto() != null)) {
387									contact.setSystemAccount(null);
388									contact.setPhotoUri(null);
389									databaseBackend.updateContact(contact);
390								}
391							}
392						}
393					}
394				});
395	}
396
397	public void addConversation(Conversation conversation) {
398		databaseBackend.createConversation(conversation);
399	}
400
401	public List<Conversation> getConversations() {
402		if (this.conversations == null) {
403			Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
404			for (Account account : this.accounts) {
405				accountLookupTable.put(account.getUuid(), account);
406			}
407			this.conversations = databaseBackend
408					.getConversations(Conversation.STATUS_AVAILABLE);
409			for (Conversation conv : this.conversations) {
410				Account account = accountLookupTable.get(conv.getAccountUuid());
411				conv.setAccount(account);
412				conv.setContact(findContact(account, conv.getContactJid()));
413				conv.setMessages(databaseBackend.getMessages(conv, 50));
414			}
415		}
416		return this.conversations;
417	}
418
419	public List<Account> getAccounts() {
420		return this.accounts;
421	}
422	
423	public Contact findContact(Account account, String jid) {
424		return databaseBackend.findContact(account, jid);
425	}
426
427	public Conversation findOrCreateConversation(Account account,
428			String jid, boolean muc) {
429		for (Conversation conv : this.getConversations()) {
430			if ((conv.getAccount().equals(account))
431					&& (conv.getContactJid().equals(jid))) {
432				return conv;
433			}
434		}
435		Conversation conversation = databaseBackend.findConversation(account,jid);
436		if (conversation != null) {
437			conversation.setStatus(Conversation.STATUS_AVAILABLE);
438			conversation.setAccount(account);
439			if (muc) {
440				conversation.setMode(Conversation.MODE_MULTI);
441				if (account.getStatus() == Account.STATUS_ONLINE) {
442					joinMuc(account, conversation);
443				}
444			} else {
445				conversation.setMode(Conversation.MODE_SINGLE);
446			}
447			this.databaseBackend.updateConversation(conversation);
448		} else {
449			String conversationName;
450			Contact contact = findContact(account, jid);
451			if (contact!=null) {
452				conversationName = contact.getDisplayName();
453			} else {
454				conversationName = jid.split("@")[0];
455			}
456			if (muc) {
457				conversation = new Conversation(conversationName,account, jid,
458						Conversation.MODE_MULTI);
459				if (account.getStatus() == Account.STATUS_ONLINE) {
460					joinMuc(account, conversation);
461				}
462			} else {
463				conversation = new Conversation(conversationName, account, jid,
464						Conversation.MODE_SINGLE);
465			}
466			conversation.setContact(contact);
467			this.databaseBackend.createConversation(conversation);
468		}
469		this.conversations.add(conversation);
470		if (this.convChangedListener != null) {
471			this.convChangedListener.onConversationListChanged();
472		}
473		return conversation;
474	}
475
476	public void archiveConversation(Conversation conversation) {
477		this.databaseBackend.updateConversation(conversation);
478		this.conversations.remove(conversation);
479		if (this.convChangedListener != null) {
480			this.convChangedListener.onConversationListChanged();
481		}
482	}
483
484	public int getConversationCount() {
485		return this.databaseBackend.getConversationCount();
486	}
487
488	public void createAccount(Account account) {
489		databaseBackend.createAccount(account);
490		this.accounts.add(account);
491		this.connections.put(account, this.createConnection(account));
492		if (accountChangedListener != null)
493			accountChangedListener.onAccountListChangedListener();
494	}
495
496	public void updateAccount(Account account) {
497		databaseBackend.updateAccount(account);
498		XmppConnection connection = this.connections.get(account);
499		if (connection != null) {
500			connection.disconnect();
501			this.connections.remove(account);
502		}
503		if (!account.isOptionSet(Account.OPTION_DISABLED)) {
504			this.connections.put(account, this.createConnection(account));
505		} else {
506			Log.d(LOGTAG, account.getJid()
507					+ ": not starting because it's disabled");
508		}
509		if (accountChangedListener != null)
510			accountChangedListener.onAccountListChangedListener();
511	}
512
513	public void deleteAccount(Account account) {
514		Log.d(LOGTAG, "called delete account");
515		if (this.connections.containsKey(account)) {
516			Log.d(LOGTAG, "found connection. disconnecting");
517			this.connections.get(account).disconnect();
518			this.connections.remove(account);
519			this.accounts.remove(account);
520		}
521		databaseBackend.deleteAccount(account);
522		if (accountChangedListener != null)
523			accountChangedListener.onAccountListChangedListener();
524	}
525
526	public void setOnConversationListChangedListener(
527			OnConversationListChangedListener listener) {
528		this.convChangedListener = listener;
529	}
530
531	public void removeOnConversationListChangedListener() {
532		this.convChangedListener = null;
533	}
534
535	public void setOnAccountListChangedListener(
536			OnAccountListChangedListener listener) {
537		this.accountChangedListener = listener;
538	}
539
540	public void removeOnAccountListChangedListener() {
541		this.accountChangedListener = null;
542	}
543
544	public void connectMultiModeConversations(Account account) {
545		List<Conversation> conversations = getConversations();
546		for (int i = 0; i < conversations.size(); i++) {
547			Conversation conversation = conversations.get(i);
548			if ((conversation.getMode() == Conversation.MODE_MULTI)
549					&& (conversation.getAccount() == account)) {
550				joinMuc(account, conversation);
551			}
552		}
553	}
554
555	public void joinMuc(Account account, Conversation conversation) {
556		String muc = conversation.getContactJid();
557		PresencePacket packet = new PresencePacket();
558		packet.setAttribute("to", muc + "/" + account.getUsername());
559		Element x = new Element("x");
560		x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
561		if (conversation.getMessages().size()!=0) {
562			Element history = new Element("history");
563			history.setAttribute("seconds",(System.currentTimeMillis() - conversation.getLatestMessageDate()) / 1000+"");
564			x.addChild(history);
565		} else {
566			Log.d(LOGTAG,"conversation had no prior messages"+conversation.getMessages().size());
567		}
568		packet.addChild(x);
569		Log.d(LOGTAG,packet.toString());
570		connections.get(conversation.getAccount()).sendPresencePacket(packet);
571	}
572
573	public void disconnectMultiModeConversations() {
574
575	}
576
577	@Override
578	public IBinder onBind(Intent intent) {
579		return mBinder;
580	}
581}