XmppConnectionService.java

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