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