1package de.gultsch.chat.services;
2
3import java.text.ParseException;
4import java.text.SimpleDateFormat;
5import java.util.ArrayList;
6import java.util.Date;
7import java.util.Hashtable;
8import java.util.List;
9
10import de.gultsch.chat.entities.Account;
11import de.gultsch.chat.entities.Contact;
12import de.gultsch.chat.entities.Conversation;
13import de.gultsch.chat.entities.Message;
14import de.gultsch.chat.entities.Presences;
15import de.gultsch.chat.persistance.DatabaseBackend;
16import de.gultsch.chat.ui.OnAccountListChangedListener;
17import de.gultsch.chat.ui.OnConversationListChangedListener;
18import de.gultsch.chat.ui.OnRosterFetchedListener;
19import de.gultsch.chat.utils.OnPhoneContactsLoadedListener;
20import de.gultsch.chat.utils.PhoneHelper;
21import de.gultsch.chat.utils.UIHelper;
22import de.gultsch.chat.xml.Element;
23import de.gultsch.chat.xmpp.IqPacket;
24import de.gultsch.chat.xmpp.MessagePacket;
25import de.gultsch.chat.xmpp.OnIqPacketReceived;
26import de.gultsch.chat.xmpp.OnMessagePacketReceived;
27import de.gultsch.chat.xmpp.OnPresencePacketReceived;
28import de.gultsch.chat.xmpp.OnStatusChanged;
29import de.gultsch.chat.xmpp.PresencePacket;
30import de.gultsch.chat.xmpp.XmppConnection;
31import android.app.NotificationManager;
32import android.app.Service;
33import android.content.Context;
34import android.content.Intent;
35import android.database.ContentObserver;
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 ContentObserver contactObserver = new ContentObserver(null) {
59 @Override
60 public void onChange(boolean selfChange) {
61 super.onChange(selfChange);
62 Log.d(LOGTAG, "contact list has changed");
63 mergePhoneContactsWithRoster();
64 }
65 };
66
67 private final IBinder mBinder = new XmppConnectionBinder();
68 private OnMessagePacketReceived messageListener = new OnMessagePacketReceived() {
69
70 @Override
71 public void onMessagePacketReceived(Account account,
72 MessagePacket packet) {
73 if ((packet.getType() == MessagePacket.TYPE_CHAT)
74 || (packet.getType() == MessagePacket.TYPE_GROUPCHAT)) {
75 boolean notify = true;
76 int status = Message.STATUS_RECIEVED;
77 String body;
78 String fullJid;
79 if (!packet.hasChild("body")) {
80 Element forwarded;
81 if (packet.hasChild("received")) {
82 forwarded = packet.findChild("received").findChild(
83 "forwarded");
84 } else if (packet.hasChild("sent")) {
85 forwarded = packet.findChild("sent").findChild(
86 "forwarded");
87 status = Message.STATUS_SEND;
88 notify = false;
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 return;
119 }
120 counterPart = fromParts[1];
121 if (counterPart.equals(account.getUsername())) {
122 status = Message.STATUS_SEND;
123 notify = false;
124 }
125 } else {
126 counterPart = fullJid;
127 }
128 Message message = new Message(conversation, counterPart, body,
129 Message.ENCRYPTION_NONE, status);
130 if (packet.hasChild("delay")) {
131 try {
132 String stamp = packet.findChild("delay").getAttribute(
133 "stamp");
134 stamp = stamp.replace("Z", "+0000");
135 Date date = new SimpleDateFormat(
136 "yyyy-MM-dd'T'HH:mm:ssZ").parse(stamp);
137 message.setTime(date.getTime());
138 } catch (ParseException e) {
139 Log.d(LOGTAG,
140 "error trying to parse date" + e.getMessage());
141 }
142 }
143 if (notify) {
144 message.markUnread();
145 }
146 conversation.getMessages().add(message);
147 databaseBackend.createMessage(message);
148 if (convChangedListener != null) {
149 convChangedListener.onConversationListChanged();
150 } else {
151 if (notify) {
152 NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
153 mNotificationManager.notify(2342, UIHelper
154 .getUnreadMessageNotification(
155 getApplicationContext(), conversation));
156 }
157 }
158 }
159 }
160 };
161 private OnStatusChanged statusListener = new OnStatusChanged() {
162
163 @Override
164 public void onStatusChanged(Account account) {
165 if (accountChangedListener != null) {
166 accountChangedListener.onAccountListChangedListener();
167 }
168 if (account.getStatus() == Account.STATUS_ONLINE) {
169 databaseBackend.clearPresences(account);
170 connectMultiModeConversations(account);
171 List<Conversation> conversations = getConversations();
172 for(int i = 0; i < conversations.size(); ++i) {
173 if (conversations.get(i).getAccount()==account) {
174 sendUnsendMessages(conversations.get(i));
175 }
176 }
177 if (convChangedListener!=null) {
178 convChangedListener.onConversationListChanged();
179 }
180 }
181 }
182 };
183
184 private OnPresencePacketReceived presenceListener = new OnPresencePacketReceived() {
185
186 @Override
187 public void onPresencePacketReceived(Account account,
188 PresencePacket packet) {
189 String[] fromParts = packet.getAttribute("from").split("/");
190 Contact contact = findContact(account, fromParts[0]);
191 if (contact == null) {
192 // most likely muc, self or roster not synced
193 // Log.d(LOGTAG,"got presence for non contact "+packet.toString());
194 return;
195 }
196 String type = packet.getAttribute("type");
197 if (type == null) {
198 Element show = packet.findChild("show");
199 if (show == null) {
200 contact.updatePresence(fromParts[1], Presences.ONLINE);
201 } else if (show.getContent().equals("away")) {
202 contact.updatePresence(fromParts[1], Presences.AWAY);
203 } else if (show.getContent().equals("xa")) {
204 contact.updatePresence(fromParts[1], Presences.XA);
205 } else if (show.getContent().equals("chat")) {
206 contact.updatePresence(fromParts[1], Presences.CHAT);
207 } else if (show.getContent().equals("dnd")) {
208 contact.updatePresence(fromParts[1], Presences.DND);
209 }
210 databaseBackend.updateContact(contact);
211 } else if (type.equals("unavailable")) {
212 if (fromParts.length != 2) {
213 // Log.d(LOGTAG,"received presence with no resource "+packet.toString());
214 } else {
215 contact.removePresence(fromParts[1]);
216 databaseBackend.updateContact(contact);
217 }
218 }
219 }
220 };
221
222 public class XmppConnectionBinder extends Binder {
223 public XmppConnectionService getService() {
224 return XmppConnectionService.this;
225 }
226 }
227
228 @Override
229 public int onStartCommand(Intent intent, int flags, int startId) {
230 for (Account account : accounts) {
231 if (!connections.containsKey(account)) {
232 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
233 this.connections.put(account,
234 this.createConnection(account));
235 } else {
236 Log.d(LOGTAG, account.getJid()
237 + ": not starting because it's disabled");
238 }
239 }
240 }
241 return START_STICKY;
242 }
243
244 @Override
245 public void onCreate() {
246 databaseBackend = DatabaseBackend.getInstance(getApplicationContext());
247 this.accounts = databaseBackend.getAccounts();
248
249 getContentResolver().registerContentObserver(
250 ContactsContract.Contacts.CONTENT_URI, true, contactObserver);
251 }
252
253 public XmppConnection createConnection(Account account) {
254 PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
255 XmppConnection connection = new XmppConnection(account, pm);
256 connection.setOnMessagePacketReceivedListener(this.messageListener);
257 connection.setOnStatusChangedListener(this.statusListener);
258 connection.setOnPresencePacketReceivedListener(this.presenceListener);
259 Thread thread = new Thread(connection);
260 thread.start();
261 return connection;
262 }
263
264 public void sendMessage(Account account, Message message) {
265
266 if (account.getStatus() == Account.STATUS_ONLINE) {
267 MessagePacket packet = prepareMessagePacket(account, message);
268 connections.get(account).sendMessagePacket(packet);
269 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
270 message.setStatus(Message.STATUS_SEND);
271 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
272 databaseBackend.createMessage(message);
273 message.getConversation().getMessages().add(message);
274 if (convChangedListener!=null) {
275 convChangedListener.onConversationListChanged();
276 }
277 }
278 }
279 } else {
280 message.getConversation().getMessages().add(message);
281 databaseBackend.createMessage(message);
282 if (convChangedListener!=null) {
283 convChangedListener.onConversationListChanged();
284 }
285 }
286
287 }
288
289 private void sendUnsendMessages(Conversation conversation) {
290 for (int i = 0; i < conversation.getMessages().size(); ++i) {
291 if (conversation.getMessages().get(i).getStatus() == Message.STATUS_UNSEND) {
292 Message message = conversation.getMessages()
293 .get(i);
294 MessagePacket packet = prepareMessagePacket(
295 conversation.getAccount(),message);
296 connections.get(conversation.getAccount()).sendMessagePacket(
297 packet);
298 message.setStatus(Message.STATUS_SEND);
299 if (conversation.getMode() == Conversation.MODE_SINGLE) {
300 databaseBackend.updateMessage(message);
301 } else {
302 databaseBackend.deleteMessage(message);
303 conversation.getMessages().remove(i);
304 i--;
305 }
306 }
307 }
308 }
309
310 private MessagePacket prepareMessagePacket(Account account, Message message) {
311 MessagePacket packet = new MessagePacket();
312 if (message.getConversation().getMode() == Conversation.MODE_SINGLE) {
313 packet.setType(MessagePacket.TYPE_CHAT);
314 } else if (message.getConversation().getMode() == Conversation.MODE_MULTI) {
315 packet.setType(MessagePacket.TYPE_GROUPCHAT);
316 }
317 packet.setTo(message.getCounterpart());
318 packet.setFrom(account.getJid());
319 packet.setBody(message.getBody());
320 return packet;
321 }
322
323 public void getRoster(Account account,
324 final OnRosterFetchedListener listener) {
325 List<Contact> contacts = databaseBackend.getContacts(account);
326 for (int i = 0; i < contacts.size(); ++i) {
327 contacts.get(i).setAccount(account);
328 }
329 if (listener != null) {
330 listener.onRosterFetched(contacts);
331 }
332 }
333
334 public void updateRoster(final Account account,
335 final OnRosterFetchedListener listener) {
336
337 PhoneHelper.loadPhoneContacts(this,
338 new OnPhoneContactsLoadedListener() {
339
340 @Override
341 public void onPhoneContactsLoaded(
342 final Hashtable<String, Bundle> phoneContacts) {
343 IqPacket iqPacket = new IqPacket(IqPacket.TYPE_GET);
344 Element query = new Element("query");
345 query.setAttribute("xmlns", "jabber:iq:roster");
346 query.setAttribute("ver", "");
347 iqPacket.addChild(query);
348 connections.get(account).sendIqPacket(iqPacket,
349 new OnIqPacketReceived() {
350
351 @Override
352 public void onIqPacketReceived(
353 Account account, IqPacket packet) {
354 List<Contact> contacts = new ArrayList<Contact>();
355 Element roster = packet
356 .findChild("query");
357 if (roster != null) {
358 for (Element item : roster
359 .getChildren()) {
360 Contact contact;
361 String name = item
362 .getAttribute("name");
363 String jid = item
364 .getAttribute("jid");
365 if (phoneContacts
366 .containsKey(jid)) {
367 Bundle phoneContact = phoneContacts
368 .get(jid);
369 String systemAccount = phoneContact
370 .getInt("phoneid")
371 + "#"
372 + phoneContact
373 .getString("lookup");
374 contact = new Contact(
375 account,
376 phoneContact
377 .getString("displayname"),
378 jid,
379 phoneContact
380 .getString("photouri"));
381 contact.setSystemAccount(systemAccount);
382 } else {
383 if (name == null) {
384 name = jid.split("@")[0];
385 }
386 contact = new Contact(
387 account, name, jid,
388 null);
389
390 }
391 contact.setAccount(account);
392 contact.setSubscription(item
393 .getAttribute("subscription"));
394 contacts.add(contact);
395 }
396 databaseBackend
397 .mergeContacts(contacts);
398 if (listener != null) {
399 listener.onRosterFetched(contacts);
400 }
401 }
402 }
403 });
404
405 }
406 });
407 }
408
409 public void mergePhoneContactsWithRoster() {
410 PhoneHelper.loadPhoneContacts(this,
411 new OnPhoneContactsLoadedListener() {
412 @Override
413 public void onPhoneContactsLoaded(
414 Hashtable<String, Bundle> phoneContacts) {
415 List<Contact> contacts = databaseBackend
416 .getContacts(null);
417 for (int i = 0; i < contacts.size(); ++i) {
418 Contact contact = contacts.get(i);
419 if (phoneContacts.containsKey(contact.getJid())) {
420 Bundle phoneContact = phoneContacts.get(contact
421 .getJid());
422 String systemAccount = phoneContact
423 .getInt("phoneid")
424 + "#"
425 + phoneContact.getString("lookup");
426 contact.setSystemAccount(systemAccount);
427 contact.setPhotoUri(phoneContact
428 .getString("photouri"));
429 contact.setDisplayName(phoneContact
430 .getString("displayname"));
431 databaseBackend.updateContact(contact);
432 } else {
433 if ((contact.getSystemAccount() != null)
434 || (contact.getProfilePhoto() != null)) {
435 contact.setSystemAccount(null);
436 contact.setPhotoUri(null);
437 databaseBackend.updateContact(contact);
438 }
439 }
440 }
441 }
442 });
443 }
444
445 public void addConversation(Conversation conversation) {
446 databaseBackend.createConversation(conversation);
447 }
448
449 public List<Conversation> getConversations() {
450 if (this.conversations == null) {
451 Hashtable<String, Account> accountLookupTable = new Hashtable<String, Account>();
452 for (Account account : this.accounts) {
453 accountLookupTable.put(account.getUuid(), account);
454 }
455 this.conversations = databaseBackend
456 .getConversations(Conversation.STATUS_AVAILABLE);
457 for (Conversation conv : this.conversations) {
458 Account account = accountLookupTable.get(conv.getAccountUuid());
459 conv.setAccount(account);
460 conv.setContact(findContact(account, conv.getContactJid()));
461 conv.setMessages(databaseBackend.getMessages(conv, 50));
462 }
463 }
464 return this.conversations;
465 }
466
467 public List<Account> getAccounts() {
468 return this.accounts;
469 }
470
471 public Contact findContact(Account account, String jid) {
472 return databaseBackend.findContact(account, jid);
473 }
474
475 public Conversation findOrCreateConversation(Account account, String jid,
476 boolean muc) {
477 for (Conversation conv : this.getConversations()) {
478 if ((conv.getAccount().equals(account))
479 && (conv.getContactJid().equals(jid))) {
480 return conv;
481 }
482 }
483 Conversation conversation = databaseBackend.findConversation(account,
484 jid);
485 if (conversation != null) {
486 conversation.setStatus(Conversation.STATUS_AVAILABLE);
487 conversation.setAccount(account);
488 if (muc) {
489 conversation.setMode(Conversation.MODE_MULTI);
490 if (account.getStatus() == Account.STATUS_ONLINE) {
491 joinMuc(account, conversation);
492 }
493 } else {
494 conversation.setMode(Conversation.MODE_SINGLE);
495 }
496 this.databaseBackend.updateConversation(conversation);
497 } else {
498 String conversationName;
499 Contact contact = findContact(account, jid);
500 if (contact != null) {
501 conversationName = contact.getDisplayName();
502 } else {
503 conversationName = jid.split("@")[0];
504 }
505 if (muc) {
506 conversation = new Conversation(conversationName, account, jid,
507 Conversation.MODE_MULTI);
508 if (account.getStatus() == Account.STATUS_ONLINE) {
509 joinMuc(account, conversation);
510 }
511 } else {
512 conversation = new Conversation(conversationName, account, jid,
513 Conversation.MODE_SINGLE);
514 }
515 conversation.setContact(contact);
516 this.databaseBackend.createConversation(conversation);
517 }
518 this.conversations.add(conversation);
519 if (this.convChangedListener != null) {
520 this.convChangedListener.onConversationListChanged();
521 }
522 return conversation;
523 }
524
525 public void archiveConversation(Conversation conversation) {
526 this.databaseBackend.updateConversation(conversation);
527 this.conversations.remove(conversation);
528 if (this.convChangedListener != null) {
529 this.convChangedListener.onConversationListChanged();
530 }
531 }
532
533 public int getConversationCount() {
534 return this.databaseBackend.getConversationCount();
535 }
536
537 public void createAccount(Account account) {
538 databaseBackend.createAccount(account);
539 this.accounts.add(account);
540 this.connections.put(account, this.createConnection(account));
541 if (accountChangedListener != null)
542 accountChangedListener.onAccountListChangedListener();
543 }
544
545 public void updateAccount(Account account) {
546 databaseBackend.updateAccount(account);
547 XmppConnection connection = this.connections.get(account);
548 if (connection != null) {
549 connection.disconnect();
550 this.connections.remove(account);
551 }
552 if (!account.isOptionSet(Account.OPTION_DISABLED)) {
553 this.connections.put(account, this.createConnection(account));
554 } else {
555 Log.d(LOGTAG, account.getJid()
556 + ": not starting because it's disabled");
557 }
558 if (accountChangedListener != null)
559 accountChangedListener.onAccountListChangedListener();
560 }
561
562 public void deleteAccount(Account account) {
563 Log.d(LOGTAG, "called delete account");
564 if (this.connections.containsKey(account)) {
565 Log.d(LOGTAG, "found connection. disconnecting");
566 this.connections.get(account).disconnect();
567 this.connections.remove(account);
568 }
569 databaseBackend.deleteAccount(account);
570 this.accounts.remove(account);
571 if (accountChangedListener != null)
572 accountChangedListener.onAccountListChangedListener();
573 }
574
575 public void setOnConversationListChangedListener(
576 OnConversationListChangedListener listener) {
577 this.convChangedListener = listener;
578 }
579
580 public void removeOnConversationListChangedListener() {
581 this.convChangedListener = null;
582 }
583
584 public void setOnAccountListChangedListener(
585 OnAccountListChangedListener listener) {
586 this.accountChangedListener = listener;
587 }
588
589 public void removeOnAccountListChangedListener() {
590 this.accountChangedListener = null;
591 }
592
593 public void connectMultiModeConversations(Account account) {
594 List<Conversation> conversations = getConversations();
595 for (int i = 0; i < conversations.size(); i++) {
596 Conversation conversation = conversations.get(i);
597 if ((conversation.getMode() == Conversation.MODE_MULTI)
598 && (conversation.getAccount() == account)) {
599 joinMuc(account, conversation);
600 }
601 }
602 }
603
604 public void joinMuc(Account account, Conversation conversation) {
605 String muc = conversation.getContactJid();
606 PresencePacket packet = new PresencePacket();
607 packet.setAttribute("to", muc + "/" + account.getUsername());
608 Element x = new Element("x");
609 x.setAttribute("xmlns", "http://jabber.org/protocol/muc");
610 if (conversation.getMessages().size() != 0) {
611 Element history = new Element("history");
612 history.setAttribute(
613 "seconds",
614 (System.currentTimeMillis() - conversation
615 .getLatestMessageDate()) / 1000 + "");
616 x.addChild(history);
617 }
618 packet.addChild(x);
619 connections.get(conversation.getAccount()).sendPresencePacket(packet);
620 }
621
622 public void disconnectMultiModeConversations() {
623
624 }
625
626 @Override
627 public IBinder onBind(Intent intent) {
628 return mBinder;
629 }
630}