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