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