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}