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