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